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を読む習慣を付けておくと、性能問題を早く見つけられます。