Ruby on Rails

Active Record(アクティブレコード)とは?仕組みと発行SQL・N+1・検証の落とし穴を実行して解説【Rails 8.1.4】

Active Record(アクティブレコード)とは?仕組みと発行SQL・N+1・検証の落とし穴を実行して解説【Rails 8.1.4】

Active Record(アクティブレコード)は、データベースのテーブルの1行を1つのオブジェクトとして扱い、そのオブジェクト自身に保存や検索の処理を持たせる設計パターンです。同じ名前で、そのパターンをRubyで実装したRuby on Railsのライブラリ(O/Rマッパー)も指します。この記事では、Active Record 8.1.4をSQLite上で実際に動かし、各メソッドがどんなSQLを何本発行するかを示しながら、命名規約、CRUD、関連付け、N+1問題、バリデーションをすり抜ける操作までを解説します。

まとめ:Active Recordの要点

  • Active Recordは、Martin Fowlerが『Patterns of Enterprise Application Architecture』(2002年11月刊)で名付けた設計パターンです。RailsのActive Recordはその代表的な実装で、MVCのモデルを担います。
  • クラスがテーブル、インスタンスが行、属性がカラムに対応します。Userならusersテーブル、主キーはid、外部キーはuser_idという命名規約に従えば設定は要りません。
  • findは見つからないと例外、find_byはnilを返します。通常のバリデーション失敗ではsaveはfalse、save!は例外です。DB制約違反などでは、saveも例外を送出します。
  • whereやscopeは組み立てただけではSQLを発行しません。結果が要る時点で1本のSQLにまとまります。
  • 関連をループでたどると、ユーザー100件で101本のSQLが出ます(N+1)。includesで2本、eager_loadで1本に減りました。
  • validates uniquenessは保存前のSELECTによる確認にすぎません。insert_allやupdate_allは検証もコールバックも通らないので、重複を確実に防ぐのはDBのユニークインデックスです。

Active Recordとは:設計パターンとRailsのライブラリ

「Active Record」という語が指すものは2つあります。抽象的な設計パターンと、Railsに含まれる具体的なライブラリです。検索で出てくる説明が噛み合わないときは、たいていこの2つが混ざっています。

Martin Fowlerが定義した設計パターン

パターンとしての定義は、Fowlerのカタログに「データベースのテーブルやビューの1行をラップし、データベースアクセスをカプセル化し、そのデータにドメインロジックを加えるオブジェクト」とあります(Martin Fowler「Active Record」)。要点は、データと、それを保存・読み出す処理が同じクラスに同居することです。user.saveと書けば、そのオブジェクトが自分でINSERTやUPDATEを発行します。

パターン名の出どころは、2002年11月にAddison-Wesleyから出たFowlerの著書『Patterns of Enterprise Application Architecture』です(InformIT の書誌)。Ruby専用の考え方ではなく、YiiのAPIドキュメントはyii\db\ActiveRecordを「Active Record設計パターンの実装」と書き(Yii 2.0 API)、Djangoも設計方針で「モデルはFowlerのActive Record設計パターンに従う」と明記しています(Django design philosophies)。逆に、オブジェクトと保存処理を別クラスに分けるのがData Mapperパターンで、両者の使い分けは後半の「向かない場面」で扱います。O/Rマッパー全般の仕組みはORMとは?O/Rマッパーの仕組み・メリットとデメリットにまとめています。

RailsのActive Record(O/Rマッパー)

RailsのActive Recordは、このパターンをRubyで実装したgem(activerecord)です。David Heinemeier HanssonがBasecampの開発から切り出し、2004年5月30日に最初の一般公開版0.7.5を告知しました(告知時点でBasecampの本番で3か月以上使われていました)。Rails 1.0は2005年12月13日の公開です。現在はRailsと同じ版番号で配布され、2026年10月時点の最新は2026年9月24日公開の8.1.4です(RubyGems.org の activerecord)。モデルクラスがApplicationRecordを継承するだけで、テーブルの列がそのまま属性になり、検索・保存・関連付け・検証が使えるようになります。

gemを読み込んでestablish_connectionで接続先を指定すれば、Railsの外のRubyスクリプトからも使えます。この記事の実行結果は、Railsアプリを作らずにactiverecordとsqlite3のgemだけを読み込んだRubyスクリプト(Ruby 4.0.6・Active Record 8.1.4・sqlite3 2.10.1・インメモリDB)で取ったものです。

require "active_record"
require "sqlite3"

ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")

命名規約によるクラスとテーブルの対応

Active Recordが設定ファイルなしで動くのは、「設定より規約(Convention over Configuration)」で対応関係を名前から決めるからです。規約どおりに名前を付ければ何も書かなくてよく、外れるときだけ明示します。

クラス名・テーブル名・キーの対応

テーブル名はクラス名を小文字のスネークケースにして複数形にしたものです。英語の不規則な複数形も変換されます。実際にtable_nameとforeign_keyを出力した結果が次の表です。

モデル(クラス) テーブル名 外部キー
User users user_id
BlogPost blog_posts blog_post_id
Person people person_id
Mouse mice mouse_id

主キーはidです。created_atとupdated_atという名前の列があれば、作成時と更新時に自動で日時が入ります。後述のcreateの実行結果でも、この2列はコードで渡していないのにINSERT文に含まれています。

既存テーブルに合わせたテーブル名・主キーの上書き

既存システムのDBを読むなど、テーブル名や主キーが規約と違う場合は、モデルにtable_nameとprimary_keyを書きます。

class Member < ApplicationRecord
  self.table_name = "tbl_member"
  self.primary_key = "member_no"
end

m = Member.create!(name: "a")
m.id          # => 1(member_no の値が id としても読める)
Member.find(1)

上書きは動きますが、すべてのモデルで書くようならActive Recordの利点が薄れています。新規に設計するテーブルは規約に合わせるのが基本です。

モデルを作ってCRUDする:発行されるSQL

ここからは、users(nameはnull: false、email、active、タイムスタンプ)とposts(user_idに外部キー)の2表で実行した結果です。

モデルとマイグレーションの作成

Railsアプリでは、モデルとテーブル定義をジェネレータで作り、マイグレーションでテーブルを作ります。この記事の検証用と同じ2表なら、次のように生成します。型の後ろの!(Rails 8.0以降)はnull: false、user:referencesはuser_id列と外部キーになります。activeの既定値trueはジェネレータでは指定できないので、生成されたマイグレーションにdefault: trueを書き足します。

bin/rails generate model User name:string! email:string active:boolean
bin/rails generate model Post user:references title:string
bin/rails db:migrate

生成されるマイグレーションのActiveRecord::Migration[8.1]の数字は、生成したRailsの版が自動で入ります。生成されるファイルの一覧や取り消し方はRailsジェネレータ(rails generate)の使い方で、実行ログ付きで解説しています。

create・find・find_by・updateが発行するSQL

モデルにはnameの必須検証、emailの一意性検証、保存前にemailを小文字にするbefore_saveを付けています。各メソッドで発行されたSQLは次のとおりです(トランザクションの開始・終了は省略)。

user = User.create(name: "Taro", email: "[email protected]")
# SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ?
# INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) RETURNING "id"

User.find_by(email: "[email protected]")
# SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ?

User.find(1)
# SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?

user.update(name: "Jiro")
# SELECT 1 AS one FROM "users" WHERE "users"."email" = ? AND "users"."id" != ? LIMIT ?
# UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ?

user.destroy
# DELETE FROM "users" WHERE "users"."id" = ?   (before_destroy などのコールバックが動く)

User.where(name: "Jiro").delete_all
# DELETE FROM "users" WHERE "users"."name" = ?  (オブジェクトを作らず、コールバックも動かない)

createは2本です。1本目のSELECT 1 AS oneは一意性検証のための存在確認で、INSERTの前に毎回走ります。保存後のemailはbefore_saveで[email protected]になっていました。updateは変更された列(nameとupdated_at)だけをSETします。同じ値でupdateをもう一度呼ぶと、存在確認のSELECTは走るものの、UPDATE文は発行されませんでした。Active Recordが変更の有無を追跡しているためです。削除はdestroyが基本で、before_destroyを付けたモデルで確かめると、destroyでは動き、delete_allでは動きませんでした。has_many :posts, dependent: :destroyで投稿2件ずつのユーザーを消すと、destroyでは投稿も消え、delete_allでは投稿2件が親のないまま残りました。

find と find_by、save と save! の違い

同じ「取得」「保存」でも、失敗したときの返り方が違います。どちらを使うかで、呼び出し側のエラー処理が変わります。

呼び出し 失敗時の結果(実行結果)
User.find(9999) ActiveRecord::RecordNotFoundを送出
User.find_by(id: 9999) nil
user.save(検証エラー) false(errorsに理由)
user.save!(検証エラー) ActiveRecord::RecordInvalidを送出
User.create(name: nil) 未保存のオブジェクト(idはnil)

注意が要るのはcreateです。検証に失敗しても例外にならず、persisted?がfalseのオブジェクトが返ります。戻り値を見ずに次の処理へ進むと、保存されていないデータを前提に動いてしまいます。失敗を握りつぶしたくない処理(バッチやシードなど)ではcreate!・save!を使い、画面に検証エラーを返すコントローラではsaveの戻り値で分岐する、という使い分けが基本です。

バリデーション・関連付け・scope・コールバック

Active RecordがSQLの置き換えにとどまらないのは、モデルに宣言的な機能を持たせられるからです。よく使う4つを、上の実行で使ったモデル定義で示します。

class User < ApplicationRecord
  has_many :posts
  validates :name, presence: true
  validates :email, uniqueness: true
  before_save { self.email = email&.downcase }
  scope :active, -> { where(active: true) }
end

class Post < ApplicationRecord
  belongs_to :user
end

validatesによる保存前の検証とエラー取得

validatesで書いた条件は、save・create・updateの前に自動で検査されます。nameを空にしてsaveするとfalseが返り、errors.full_messagesには"Name can't be blank"が入りました。メッセージを画面にそのまま出せるのが、DBの制約違反(例外)との違いです。

has_many・belongs_toによる1対多の関連付け

has_many :postsとbelongs_to :userを書くと、user.postsでそのユーザーの投稿一覧、post.userで書き手をたどれます。外部キーuser_idは命名規約から決まります。なお、Rails 5.0以降の新規アプリでは、belongs_toは既定で関連先の存在を検証します。本記事のようなActive Record単体利用では同じ既定値にならないため、必要ならbelongs_to :user, optional: falseと明示します。1対多以外(多対多・中間テーブル・ポリモーフィック)の書き方はRailsのAssociationとは?6種類の関連付けと使い分けで整理しています。

where・scopeの遅延評価とSQLの実行タイミング

whereやorderは、ActiveRecord::Relationというオブジェクトを返すだけでSQLを発行しません。User.where(active: true).order(created_at: :desc)を組み立てただけでは発行0本でした。to_sqlで、実行前に組み立て結果を確認できます。

rel = User.active.where("name LIKE ?", "J%")
rel.class   # => User::ActiveRecord_Relation
rel.to_sql  # => SELECT "users".* FROM "users" WHERE "users"."active" = TRUE AND (name LIKE 'J%')

rel.count   # SELECT COUNT(*) FROM "users" WHERE ...  (1本)
rel.to_a    # SELECT "users".* FROM "users" WHERE ... (1本)
rel.size    # 読み込み済みなので発行なし

scopeは名前付きの条件で、User.active.recentのように連結しても1本のSQLにまとまります。countは読み込み済みでも毎回COUNT(*)を発行し、sizeは読み込み済みなら件数を数えるだけです。この違いは次のN+1でも効いてきます。

before_saveによる保存前の処理と検証順序

保存・作成・削除などの前後に処理を差し込む仕組みで、上の例ではbefore_saveでemailを小文字にそろえています。ただし、before_saveは検証の後に動きます。[email protected]が保存済みの状態で[email protected]を保存すると、valid?もsaveもtrueになり、小文字のメールが2件になりました。比較に使う値の正規化は、検証より前に済ませる必要があります。Rails 7.1で入ったnormalizesは代入した時点で値を整えるので、同じ操作が"Email has already been taken"で止まりました(Rails 7.1 リリースノート)。

class Account < ApplicationRecord
  normalizes :email, with: ->(e) { e.strip.downcase }
  validates :email, uniqueness: true
end

Account.new(email: " [email protected] ").email   # => "[email protected]"
Account.find_by(email: "[email protected]")        # 検索条件も正規化される

この順序の問題とは別に、コールバックをまったく通らない更新メソッドもあります。次の章で扱います。

アプリ側の検証の限界とDB制約の役割

Active Recordの検証とコールバックは、モデルのオブジェクトを経由して保存したときだけ働きます。経由しない書き込みや同時実行には効きません。この境界を知らないと、「検証を書いたのに重複データが入っている」という事故が起きます。

uniquenessの事前SELECTと同時実行時の競合

validates :email, uniqueness: trueの実体は、先ほどのログにあったSELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ?です。「今この瞬間に同じ値の行が無いか」を確かめてからINSERTするので、2つのリクエストがほぼ同時に確認を通れば、両方とも保存されます。Rails 8.1のガイドも、別々のDB接続が同じ値のレコードを作る競合はこの検証では防げず、防ぐにはその列にユニークインデックスを作る必要がある、と書いています(Active Record Validations: uniqueness)。

insert_all・update_allによる検証・コールバックの省略

一括操作のメソッドはモデルのオブジェクトを作らずにSQLを直接発行します。APIドキュメントにも、insert_allとupdate_allは検証もコールバックも実行しないと明記されています(ActiveRecord::Relation)。ユニークインデックスが無い状態で試した結果です。

User.insert_all([{ name: "Dup", email: "[email protected]" }])
# INSERT INTO "users" (...) VALUES ('Dup', '[email protected]', ...) ON CONFLICT  DO NOTHING RETURNING "id"
User.where(email: "[email protected]").count   # => 2(重複が入った)

User.where(name: "Dup").update_all(email: "[email protected]")
# UPDATE "users" SET "email" = ? WHERE "users"."name" = ?
User.find_by(name: "Dup").email               # => "[email protected]"(before_save が動いていない)

一意性検証を書いたemailに重複が入り、before_saveの小文字化も効きませんでした。update_column・update_all・insert_all・upsert_all・delete_allは、件数が多い処理では速さのために使う価値がありますが、モデルに書いたルールは素通りします。

ユニークインデックスによる重複挿入の拒否

emailにユニークインデックスを張ってから同じ操作をすると、結果が変わりました。

ActiveRecord::Base.connection.add_index :users, :email, unique: true
User.create!(name: "A", email: "[email protected]")

User.insert_all([{ name: "B", email: "[email protected]" }])   # 挿入0件(ON CONFLICT DO NOTHING で無視)
User.new(name: "C", email: "[email protected]").save(validate: false)
# => ActiveRecord::RecordNotUnique

検証を飛ばしても、DBが重複を拒否します。役割分担は、モデルの検証=利用者に分かるエラーメッセージを返すため、DBの制約(NOT NULL・ユニークインデックス・外部キー)=データの正しさを最後に保証するため、と分けて考えると判断がぶれません。必須の列にはnull: false、一意にしたい列にはユニークインデックス、関連の列には外部キーを、マイグレーションの段階で入れておきます。既存DBの制約設計やスキーマの見直しはデータベース設計・移行支援でもご相談を受けています。

N+1問題とincludes・preload・eager_load

関連をループでたどると、ユーザーごとに追加のSQLが発行されるN+1問題が起こります。ユーザー100件・各3件の投稿を用意し、発行されたSQLの本数を数えました。

User.all.each { |u| u.posts.size }
# SELECT "users".* FROM "users"
# SELECT COUNT(*) FROM "posts" WHERE "posts"."user_id" = ?   ← これがユーザー数だけ続く

User.includes(:posts).each { |u| u.posts.size }
# SELECT "users".* FROM "users"
# SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (?, ?, ...)
書き方 発行本数 取り方
そのままループ 101本 1+ユーザーごとに1本
includes(:posts) 2本 別クエリでIN先読み
preload(:posts) 2本 常に別クエリ
eager_load(:posts) 1本 LEFT OUTER JOIN
includes+where(posts: …) 1本 自動でJOINに切替

includesは通常はpreloadと同じ2本ですが、関連先の列で絞り込むとeager_loadと同じ1本のJOINに切り替わります。文字列で条件を書く場合(where("posts.title = ?", …))はreferences(:posts)を付けないとJOINになりません。時間計測では上の件数取得とは別に、各ユーザーの投稿をposts.to_aで全件読み込みました。インメモリのSQLiteで5回平均を取ると、N+1が約80ms、includesが約21msでした。ネットワーク越しのDBでは往復遅延も影響します。ただし、取得件数や転送量、DBの負荷も変わるため、性能差は実環境で計測してください。

見落としを防ぐには、Rails 6.1で入ったstrict_loadingが使えます(Rails 6.1 リリースノート)。User.strict_loading.first.posts.to_aのように先読みしていない関連を読むと、ActiveRecord::StrictLoadingViolationErrorで止まりました。また、ループ内のposts.countは先読みしても毎回COUNT(*)を発行するので、先読みした関連の件数はsizeかlengthで取ります(5ユーザーをincludesした後にcountすると7本、sizeなら2本でした)。ORMごとの比較や検出ツールはN+1問題とは?発行クエリ数の実測と検出方法で詳しく扱っています。

ActiveRecord::Base・ApplicationRecord・ActiveModelの関係

モデルの親クラスに出てくる3つの名前は、役割が階層になっています。

ActiveRecord::BaseとApplicationRecordの継承関係・設定範囲

ActiveRecord::Baseは、Active Recordの機能すべてを持つ祖先クラスです。Rails 4.2まではモデルがこれを直接継承していましたが、Rails 5.0(2016年6月30日公開)からrails newがapp/models/application_record.rbを作り、モデルはそのApplicationRecordを継承する形になりました。リリースノートは、コントローラがApplicationControllerを継承するのと同じ形にするための変更だと説明しています(Rails 5.0 リリースノート)。

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
end

abstract_class = trueは、このクラス自体はテーブルを持たないという宣言です。実行するとApplicationRecord.table_nameはnilでした。全モデルに共通のメソッドや設定はApplicationRecordに書きます。ActiveRecord::Baseを直接書き換えるとgemが定義したモデルにまで影響しますが、ApplicationRecordへの変更は自分のアプリのモデルに閉じます。

ActiveModel:DBを持たないモデル

ActiveModelは、検証・属性・コールバックなど「モデルらしさ」だけを切り出したモジュール群で、Active Recordも内部でこれを使っています。テーブルを持たないオブジェクトに検証を付けたいとき、たとえば複数のモデルにまたがる入力をまとめるフォームオブジェクトに使います。

class SignupForm
  include ActiveModel::Model

  attr_accessor :name, :email
  validates :name, :email, presence: true
end

form = SignupForm.new(name: "", email: "[email protected]")
form.valid?   # => false

DBに保存するならActive Record、保存しないが検証やフォーム連携が欲しいならActiveModel、と保存の有無で選びます。

Active Recordが向かない場面とData Mapperとの違い

Active Recordは「テーブルの形=オブジェクトの形」であるほど書きやすく、そこから離れるほど窮屈になります。弱点は2つに集約されます。

1つ目はモデルの肥大化(Fat Model)です。保存処理とビジネスロジックが同じクラスに書けるので、放置すると1つのmodel.rbが数百〜千行を超えます。Concern・サービスオブジェクト・フォームオブジェクトへの切り出し方はRailsのFat Model対策で手順を示しています。

2つ目は、ドメインの設計がテーブル構造に縛られることです。1つの業務概念を複数のテーブルに分けて保存したい、レガシーDBの列構成とは別の形でオブジェクトを組みたい、DBなしでドメインロジックを単体テストしたい、といった要件では、保存処理を別クラスに分けるData Mapperのほうが素直に書けます。FowlerはData Mapperを「オブジェクトとDBの間でデータを移しつつ、両者をお互いからもマッパー自身からも独立させるマッパーの層」と定義しています(Martin Fowler「Data Mapper」)。Rubyでは、Hanami 2.2のDB層の土台になっているROM(rom-rb)がこちらの考え方で、データアクセスはリポジトリ、データの保持は構造体と役割を分けています(Hanami 2.2 のリリース告知)。

観点 Active Record Data Mapper
保存処理の置き場所 モデル自身(user.save) 別のマッパー・リポジトリ
テーブルとの対応 ほぼ1対1 自由に変えられる
書き始めの手数 少ない 多い
Rubyの実装 Rails(activerecord) ROM(Hanami 2.2)

判断としては、CRUD画面と業務ルールが中心のWebアプリならActive Recordを選び、肥大化した部分だけをサービスオブジェクトなどに逃がします。Active Recordを避けるべきなのは、テーブル設計を変えられないレガシーDBの上に複雑なドメインモデルを組む場合や、永続化と切り離したドメイン層をチームの方針として持つ場合です。Railsの中でData Mapperに寄せるより、最初からその前提のフレームワークを選ぶほうが摩擦は小さくなります。

よくある質問

Active RecordとORM(O/Rマッパー)の違いは何ですか?

ORMは、オブジェクトとリレーショナルDBのテーブルを対応づける技術の総称です。Active Recordは、そのORMの作り方の1つ(オブジェクト自身が保存処理を持つ方式)を指すパターン名で、RailsのActive Recordはその実装です。すべてのORMがActive Record型ではなく、Data Mapper型のORMもあります。

「ActiveRecord」と「Active Record」はどちらが正しい表記ですか?

どちらも同じものを指し、読みはどちらも「アクティブレコード」です。パターン名とRailsガイドでは2語の「Active Record」、Rubyのモジュール名はActiveRecord、gem名は小文字のactiverecordと書かれるため、表記が揺れています。

Ruby以外の言語にもActive Recordはありますか?

あります。Active Recordは言語に依存しない設計パターンなので、PHPのLaravel EloquentやYiiのActive Record、PythonのDjangoのモデルなど、同じ方式のORMが各言語にあります。名前に「Active Record」と付いていなくても、テーブルやビューの行を表すオブジェクト自身がDBアクセスとドメインロジックを担うなら、このパターンに当たります。

Active Recordのバージョンはどう確認しますか?

Active RecordはRailsと同じ版番号で配布されます。Railsアプリならbin/rails -vかGemfile.lockのactiverecordの行、コードからはActiveRecord.versionで確認できます。この記事は8.1.4で実行しました。7.2で入った変更点はActiveRecord 7.2の変更点にまとめています。

Active Recordを使えばSQLは覚えなくてよいですか?

単純なCRUDなら書かずに済みますが、読めないと困ります。N+1や不要なCOUNT(*)はログに出たSQLを見て初めて気づくものですし、集計や複雑な結合はto_sqlで確かめながら組み立てることになります。開発中はlog/development.logに出るSQLを読む習慣を付けておくと、性能問題を早く見つけられます。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.29 テックブログ ソニー生命の不正アクセス:38.5万件の対象確認と窓口、停止中の手続き
  2. 2026.10.31 テックブログ 松屋の不正アクセス:237万件の対象確認と窓口、漏れたパスワードの守り方
  3. 2026.10.31 テックブログ 無印良品の情報漏洩:MUJIアプリ最大2,726万件の対象確認となりすましメール対策
  4. 2026.10.27 テックブログ Joshin(ジョーシン)不正アクセス:最大600万件の対象とサブスク初期パスワードの対処
  5. 2026.10.28 テックブログ リンツ不正アクセス:314万件の対象確認とパスワード強制リセット後の対処

RELATED POSTS 関連記事

目次