ActiveRecord

ActiveRecordは「Ruby on Railsの中核を成すORM(Object-Relational Mapping)ライブラリ」として開発された、Rubyエコシステムで最も重要なデータベース操作ライブラリです。Martin Fowlerの「Active Record」パターンに基づいて設計され、データベースのテーブルとRubyクラスを自動的にマッピング。直感的で読みやすいAPIを通じて、SQL文を書くことなく複雑なデータベース操作を実現し、Rails開発者にとって必須のツールとして確立されています。

ORMRubyRailsデータベースActiveRecordSQLモデル

GitHub概要

rails/rails

Ruby on Rails

スター57,276
ウォッチ2,311
フォーク21,912
作成日:2008年4月11日
言語:Ruby
ライセンス:MIT License

トピックス

activejobactiverecordframeworkhtmlmvcrailsruby

スター履歴

rails/rails Star History
データ取得日時: 2025/8/13 01:43

ライブラリ

ActiveRecord

概要

ActiveRecordは「Ruby on Railsの中核を成すORM(Object-Relational Mapping)ライブラリ」として開発された、Rubyエコシステムで最も重要なデータベース操作ライブラリです。Martin Fowlerの「Active Record」パターンに基づいて設計され、データベースのテーブルとRubyクラスを自動的にマッピング。直感的で読みやすいAPIを通じて、SQL文を書くことなく複雑なデータベース操作を実現し、Rails開発者にとって必須のツールとして確立されています。

詳細

ActiveRecord 2025年版はRuby on Railsフレームワークの基盤として15年以上の実績を持ち、現代的なRubyアプリケーション開発における標準的なORM解決策です。Convention over Configuration(設定より規約)の哲学に従い、最小限の設定でリレーショナルデータベースとの強力な統合を提供。マイグレーション、バリデーション、アソシエーション、スコープ、コールバックなど豊富な機能により、企業レベルのデータベース駆動アプリケーション開発を効率化。MySQL、PostgreSQL、SQLite、SQL Serverなど主要なデータベースエンジンに対応し、開発から本番環境まで一貫した開発体験を提供します。

主な特徴

  • 自動マッピング: テーブル名とクラス名の自動的な対応付けとスキーマの自動検出
  • 豊富なアソシエーション: belongs_to、has_many、has_one等の直感的な関連定義
  • スマートクエリAPI: where、joins、includes等の読みやすいクエリ構築
  • マイグレーション: データベーススキーマの版数管理と自動適用
  • バリデーション: モデルレベルでのデータ整合性チェック機能
  • トランザクション: 自動ロールバック機能付きのデータベーストランザクション

メリット・デメリット

メリット

  • Railsエコシステムでの圧倒的な普及率と豊富なドキュメント・学習リソース
  • Convention over Configurationによる設定レスな開発と高い生産性
  • 豊富なアソシエーション機能による複雑なデータ関係の直感的な表現
  • マイグレーション機能によるチーム開発でのスキーマ同期の簡素化
  • スコープ機能による再利用可能なクエリロジックの構築
  • バリデーション機能による堅牢なデータ整合性の確保

デメリット

  • 大量データ処理時のメモリ使用量増加とパフォーマンス制約
  • 複雑なSQLクエリの表現に限界があり生SQLが必要な場合がある
  • N+1クエリ問題が発生しやすく適切なeager loadingが必要
  • テーブル命名規約に従わない既存データベースとの統合が困難
  • 巨大なモデルクラスによるコード保守性の悪化
  • データベース固有機能の利用が制限される場合がある

参考ページ

書き方の例

基本セットアップ

# Gemfile
gem 'rails', '~> 7.1.0'

# データベース設定(config/database.yml)
development:
  adapter: postgresql
  database: myapp_development
  username: postgres
  password: password
  host: localhost
  port: 5432
  
# モデルファイル生成
rails generate model User name:string email:string age:integer

# マイグレーション実行
rails db:migrate

# Railsコンソールでの動作確認
rails console

モデル定義と基本操作

# app/models/user.rb
class User < ApplicationRecord
  # バリデーション
  validates :name, presence: true, length: { minimum: 2 }
  validates :email, presence: true, uniqueness: true
  validates :age, presence: true, numericality: { greater_than: 0 }
  
  # スコープ
  scope :adults, -> { where('age >= ?', 18) }
  scope :by_name, ->(name) { where('name ILIKE ?', "%#{name}%") }
  
  # コールバック
  before_save :normalize_name
  after_create :send_welcome_email
  
  private
  
  def normalize_name
    self.name = name.strip.titleize
  end
  
  def send_welcome_email
    UserMailer.welcome(self).deliver_now
  end
end

# 基本的なCRUD操作
# 作成(Create)
user = User.new(name: "田中太郎", email: "[email protected]", age: 30)
user.save!

# または
user = User.create!(name: "佐藤花子", email: "[email protected]", age: 25)

# 読み取り(Read)
user = User.find(1)  # IDで検索
user = User.find_by(email: "[email protected]")  # 条件で検索
users = User.where(age: 20..30)  # 範囲指定
users = User.adults  # スコープ使用

# 更新(Update)
user.update!(name: "田中次郎")
User.where(age: 18).update_all(status: 'adult')

# 削除(Delete)
user.destroy!
User.where('created_at < ?', 1.year.ago).delete_all

高度なクエリ操作

# 複雑な条件の組み合わせ
users = User.where(age: 20..30)
            .where.not(email: nil)
            .where("name ILIKE ?", "%田中%")
            .order(:created_at)
            .limit(10)

# JOINクエリ
users_with_posts = User.joins(:posts)
                      .where(posts: { published: true })
                      .distinct

# サブクエリの使用
active_users = User.where(
  id: Post.where('created_at > ?', 1.month.ago)
          .select(:user_id)
          .distinct
)

# 集約関数
User.count
User.where(age: 20..30).average(:age)
User.group(:department).count
User.having('COUNT(*) > 5').group(:city).count

# SQLフラグメントの使用
User.where("age > ? AND created_at > ?", 18, 1.year.ago)
User.where("EXTRACT(year FROM created_at) = ?", 2024)

# 動的クエリ構築
def search_users(params)
  query = User.all
  
  query = query.where('name ILIKE ?', "%#{params[:name]}%") if params[:name].present?
  query = query.where(age: params[:min_age]..params[:max_age]) if params[:min_age] && params[:max_age]
  query = query.where(department: params[:department]) if params[:department].present?
  query = query.order(params[:sort_by] || :created_at)
  
  query
end

# 使用例
users = search_users({
  name: "田中",
  min_age: 20,
  max_age: 40,
  department: "engineering",
  sort_by: :name
})

リレーション操作

# アソシエーション定義
class User < ApplicationRecord
  has_many :posts, dependent: :destroy
  has_many :comments, dependent: :destroy
  has_many :authored_posts, class_name: 'Post', foreign_key: 'author_id'
  has_one :profile, dependent: :destroy
  belongs_to :department
  
  has_many :user_roles
  has_many :roles, through: :user_roles
  
  has_many :following_relationships, class_name: 'Follow', foreign_key: 'follower_id'
  has_many :following, through: :following_relationships, source: :followed
  
  has_many :follower_relationships, class_name: 'Follow', foreign_key: 'followed_id'
  has_many :followers, through: :follower_relationships, source: :follower
end

class Post < ApplicationRecord
  belongs_to :user
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy
  has_many :commenters, through: :comments, source: :user
  
  validates :title, presence: true
  validates :content, presence: true, length: { minimum: 10 }
  
  scope :published, -> { where(published: true) }
  scope :recent, -> { where('created_at > ?', 1.week.ago) }
end

class Profile < ApplicationRecord
  belongs_to :user
  validates :bio, length: { maximum: 500 }
end

class Department < ApplicationRecord
  has_many :users
  validates :name, presence: true, uniqueness: true
end

# リレーションの操作
user = User.find(1)

# 関連レコードの作成
post = user.posts.create!(title: "Hello World", content: "My first post")
profile = user.create_profile!(bio: "Software Developer")

# 関連レコードの取得(N+1問題を避ける)
users_with_posts = User.includes(:posts).where(posts: { published: true })
users_with_profiles = User.includes(:profile, :department)

# ネストした関連の取得
users_with_comments = User.includes(posts: :comments)

# 関連レコードの条件付き取得
users_with_recent_posts = User.joins(:posts)
                             .where(posts: { created_at: 1.week.ago.. })
                             .distinct

# 関連レコードの集約
users_with_post_counts = User.left_joins(:posts)
                            .group(:id)
                            .select('users.*, COUNT(posts.id) as posts_count')

# 多対多関係の操作
user = User.find(1)
role = Role.find_by(name: 'admin')

user.roles << role  # 関連付け追加
user.roles.delete(role)  # 関連付け削除
user.role_ids = [1, 2, 3]  # 関連付け一括設定

実用例

# マイグレーション例
class CreateBlogSchema < ActiveRecord::Migration[7.1]
  def change
    # ユーザーテーブル
    create_table :users do |t|
      t.string :name, null: false
      t.string :email, null: false
      t.integer :age
      t.text :bio
      t.references :department, null: true, foreign_key: true
      t.timestamps
    end
    
    add_index :users, :email, unique: true
    add_index :users, :name
    
    # 投稿テーブル
    create_table :posts do |t|
      t.string :title, null: false
      t.text :content, null: false
      t.boolean :published, default: false
      t.references :user, null: false, foreign_key: true
      t.references :author, null: true, foreign_key: { to_table: :users }
      t.timestamps
    end
    
    add_index :posts, [:user_id, :published]
    add_index :posts, :created_at
    
    # コメントテーブル
    create_table :comments do |t|
      t.text :content, null: false
      t.references :post, null: false, foreign_key: true
      t.references :user, null: false, foreign_key: true
      t.timestamps
    end
    
    # 部署テーブル
    create_table :departments do |t|
      t.string :name, null: false
      t.text :description
      t.timestamps
    end
    
    add_index :departments, :name, unique: true
  end
end

# コントローラでの使用例
class UsersController < ApplicationController
  before_action :set_user, only: [:show, :edit, :update, :destroy]
  
  def index
    @users = User.includes(:department, :posts)
                 .order(:name)
                 .page(params[:page])
  end
  
  def show
    @recent_posts = @user.posts.published.recent.limit(5)
  end
  
  def create
    @user = User.new(user_params)
    
    if @user.save
      redirect_to @user, notice: 'ユーザーが作成されました。'
    else
      render :new, status: :unprocessable_entity
    end
  end
  
  def update
    if @user.update(user_params)
      redirect_to @user, notice: 'ユーザーが更新されました。'
    else
      render :edit, status: :unprocessable_entity
    end
  end
  
  def destroy
    @user.destroy
    redirect_to users_url, notice: 'ユーザーが削除されました。'
  end
  
  private
  
  def set_user
    @user = User.find(params[:id])
  end
  
  def user_params
    params.require(:user).permit(:name, :email, :age, :bio, :department_id)
  end
end

# サービスオブジェクトでの使用例
class UserStatsService
  def self.call(user)
    new(user).call
  end
  
  def initialize(user)
    @user = user
  end
  
  def call
    {
      total_posts: user.posts.count,
      published_posts: user.posts.published.count,
      total_comments: user.comments.count,
      followers_count: user.followers.count,
      following_count: user.following.count,
      average_post_length: average_post_length,
      most_commented_post: most_commented_post
    }
  end
  
  private
  
  attr_reader :user
  
  def average_post_length
    user.posts.published.average('LENGTH(content)')&.round(2) || 0
  end
  
  def most_commented_post
    user.posts
        .joins(:comments)
        .group('posts.id')
        .order('COUNT(comments.id) DESC')
        .limit(1)
        .select('posts.*, COUNT(comments.id) as comments_count')
        .first
  end
end

# 使用例
user = User.find(1)
stats = UserStatsService.call(user)
puts "総投稿数: #{stats[:total_posts]}"
puts "公開投稿数: #{stats[:published_posts]}"
puts "平均投稿長: #{stats[:average_post_length]} 文字"