Active Record

Active RecordはRuby on RailsのORMです。Convention over Configurationの哲学により最小限の設定でActive Recordパターンを実装し、データベースマイグレーション、バリデーション、コールバック、アソシエーション機能を内蔵します。

ORMRubyRailsActive RecordWeb開発

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

ライブラリ

Active Record

概要

Active RecordはRuby on RailsのORMです。Convention over Configurationの哲学により最小限の設定でActive Recordパターンを実装し、データベースマイグレーション、バリデーション、コールバック、アソシエーション機能を内蔵します。

詳細

Active Recordは、Martin Fowlerが定義したActive Recordパターンを忠実に実装したORMです。Railsの「設定より規約」の哲学を体現し、テーブル名やカラム名の規約に従うことで、最小限のコードでデータベース操作を実現します。モデルクラスにビジネスロジックとデータアクセス機能を統合し、直感的で表現力豊かなAPIを提供します。

主な特徴

  • Convention over Configuration: 規約による設定の最小化
  • 豊富なバリデーション: データ整合性を保つ検証機能
  • アソシエーション: 直感的なリレーション定義
  • コールバック: モデルライフサイクルでのフック
  • マイグレーション: 安全なスキーマ変更管理

メリット・デメリット

メリット

  • Ruby開発者にとって自然で読みやすいAPI
  • 最小限の設定で高機能なデータベース操作を実現
  • 豊富なアソシエーション機能で複雑なデータ関係も簡潔に表現
  • Rails エコシステムとの完全な統合
  • 長年の実績による安定性と信頼性

デメリット

  • 大規模システムでのパフォーマンス課題
  • Active Recordパターンによるビジネスロジックとデータアクセスの密結合
  • Rails フレームワーク依存のため他のフレームワークでは使用困難
  • 複雑なクエリの表現に制限がある場合がある

参考ページ

書き方の例

インストールと基本セットアップ

# Rails プロジェクト作成
rails new myapp
cd myapp

# データベース設定(config/database.yml で設定)
rails generate model User name:string email:string
rails generate model Post title:string content:text user:references
# config/database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

development:
  <<: *default
  database: myapp_development
  username: myapp
  password: <%= ENV['MYAPP_DATABASE_PASSWORD'] %>

test:
  <<: *default
  database: myapp_test

production:
  <<: *default
  database: myapp_production
  username: myapp
  password: <%= ENV['MYAPP_DATABASE_PASSWORD'] %>

基本的なCRUD操作(モデル定義、作成、読み取り、更新、削除)

# app/models/user.rb
class User < ApplicationRecord
  has_many :posts, dependent: :destroy
  
  validates :name, presence: true, length: { minimum: 2, maximum: 50 }
  validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
  
  before_save :downcase_email
  
  scope :active, -> { where(status: 'active') }
  scope :recent, -> { order(created_at: :desc) }
  
  private
  
  def downcase_email
    self.email = email.downcase
  end
end

# app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
  
  validates :title, presence: true, length: { minimum: 5, maximum: 100 }
  validates :content, presence: true, length: { minimum: 10 }
  
  scope :published, -> { where.not(published_at: nil) }
  scope :by_date, -> { order(created_at: :desc) }
  
  def published?
    published_at.present?
  end
end

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

# 基本的なCRUD操作
# 作成
user = User.create(
  name: '田中太郎',
  email: '[email protected]'
)

# 読み取り
all_users = User.all
user_by_id = User.find(1)
user_by_email = User.find_by(email: '[email protected]')
active_users = User.active

# 更新
user = User.find(1)
user.update(name: '田中次郎')

# 一括更新
User.where(status: 'pending').update_all(status: 'active')

# 削除
user = User.find(1)
user.destroy

# 一括削除
User.where(status: 'inactive').destroy_all

高度なクエリとリレーションシップ

# 複雑な条件クエリ
active_users = User.where(status: 'active')
                   .where('created_at > ?', 30.days.ago)
                   .where('email LIKE ?', '%@company.com')
                   .order(created_at: :desc)
                   .limit(10)

# アソシエーションクエリ
users_with_posts = User.includes(:posts)

users_with_recent_posts = User.joins(:posts)
                              .where(posts: { created_at: 1.week.ago.. })
                              .distinct

# ネストしたアソシエーション
users_with_published_posts = User.joins(posts: :comments)
                                 .where(posts: { published_at: ..Time.current })
                                 .where(comments: { approved: true })

# 集約クエリ
user_stats = User.select('COUNT(*) as total_users, AVG(posts_count) as avg_posts')
                 .joins(:posts)
                 .group('users.id')

# カウンタクエリ
User.joins(:posts).group('users.id').count
Post.group(:user_id).count

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

# 複雑なスコープ
class User < ApplicationRecord
  scope :with_recent_activity, ->(days = 30) {
    joins(:posts).where(posts: { created_at: days.days.ago.. }).distinct
  }
  
  scope :by_post_count, ->(min_count = 1) {
    joins(:posts).group('users.id').having('COUNT(posts.id) >= ?', min_count)
  }
end

# スコープ使用例
active_productive_users = User.active
                               .with_recent_activity(7)
                               .by_post_count(3)

# 動的クエリ
def search_users(params)
  scope = User.all
  scope = scope.where('name ILIKE ?', "%#{params[:name]}%") if params[:name].present?
  scope = scope.where(status: params[:status]) if params[:status].present?
  scope = scope.where('created_at >= ?', params[:created_after]) if params[:created_after].present?
  scope
end

マイグレーションとスキーマ管理

# マイグレーション作成
rails generate migration CreateUsers name:string email:string:index
rails generate migration AddStatusToUsers status:string

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

# ロールバック
rails db:rollback
rails db:rollback STEP=3

# マイグレーション状態確認
rails db:migrate:status
# db/migrate/xxxx_create_users.rb
class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :name, null: false
      t.string :email, null: false
      t.string :status, default: 'active'
      
      t.timestamps
    end
    
    add_index :users, :email, unique: true
    add_index :users, [:status, :created_at]
  end
end

# db/migrate/xxxx_create_posts.rb
class CreatePosts < ActiveRecord::Migration[7.0]
  def change
    create_table :posts do |t|
      t.string :title, null: false
      t.text :content
      t.references :user, null: false, foreign_key: true
      t.datetime :published_at
      
      t.timestamps
    end
    
    add_index :posts, [:user_id, :published_at]
    add_index :posts, :published_at
  end
end

# カスタムマイグレーション
class AddFullTextSearchToPosts < ActiveRecord::Migration[7.0]
  def up
    execute "CREATE INDEX posts_full_text_search ON posts USING gin(to_tsvector('english', title || ' ' || content))"
  end
  
  def down
    execute "DROP INDEX posts_full_text_search"
  end
end

# データマイグレーション
class MigrateUserStatuses < ActiveRecord::Migration[7.0]
  def up
    User.where(last_login_at: nil).update_all(status: 'inactive')
    User.where('last_login_at < ?', 1.year.ago).update_all(status: 'dormant')
  end
  
  def down
    User.where(status: ['inactive', 'dormant']).update_all(status: 'active')
  end
end

パフォーマンス最適化と高度な機能

# トランザクション
ActiveRecord::Base.transaction do
  user = User.create!(
    name: '山田花子',
    email: '[email protected]'
  )
  
  post = Post.create!(
    title: '最初の投稿',
    content: 'Active Recordを使った投稿です',
    user: user,
    published_at: Time.current
  )
end

# バッチ操作
users_data = [
  { name: 'ユーザー1', email: '[email protected]' },
  { name: 'ユーザー2', email: '[email protected]' },
  { name: 'ユーザー3', email: '[email protected]' }
]

User.insert_all(users_data)

# バッチ更新
User.where(status: 'pending').update_all(
  status: 'active',
  activated_at: Time.current
)

# バッチ処理(find_each)
User.where(status: 'active').find_each(batch_size: 100) do |user|
  user.send_newsletter
end

# includesでN+1問題を回避
posts = Post.includes(:user, :comments).limit(10)

posts.each do |post|
  puts "#{post.title} by #{post.user.name} (#{post.comments.count} comments)"
end

# カウンタキャッシュ
class User < ApplicationRecord
  has_many :posts, dependent: :destroy, counter_cache: true
end

class Post < ApplicationRecord
  belongs_to :user, counter_cache: true
end

# カスタムバリデーション
class User < ApplicationRecord
  validate :email_domain_allowed
  
  private
  
  def email_domain_allowed
    allowed_domains = ['company.com', 'partner.org']
    domain = email.split('@').last
    
    unless allowed_domains.include?(domain)
      errors.add(:email, 'must be from an allowed domain')
    end
  end
end

# コールバック
class User < ApplicationRecord
  before_validation :normalize_email
  before_create :generate_uuid
  after_create :send_welcome_email
  after_update :log_changes, if: :saved_change_to_email?
  
  private
  
  def normalize_email
    self.email = email.strip.downcase
  end
  
  def generate_uuid
    self.uuid = SecureRandom.uuid
  end
  
  def send_welcome_email
    UserMailer.welcome(self).deliver_later
  end
  
  def log_changes
    Rails.logger.info "User #{id} email changed from #{email_previous_change[0]} to #{email}"
  end
end

# カスタムアトリビュート
class User < ApplicationRecord
  attribute :preferences, :json, default: {}
  
  def full_name
    "#{first_name} #{last_name}"
  end
  
  def full_name=(name)
    split = name.split(' ', 2)
    self.first_name = split.first
    self.last_name = split.last
  end
end

フレームワーク統合と実用例

# コントローラー統合
class UsersController < ApplicationController
  before_action :set_user, only: [:show, :edit, :update, :destroy]
  
  def index
    @users = User.includes(:posts)
                 .page(params[:page])
                 .per(10)
  end
  
  def show
    @posts = @user.posts.published.recent.limit(5)
  end
  
  def create
    @user = User.new(user_params)
    
    if @user.save
      redirect_to @user, notice: 'User was successfully created.'
    else
      render :new
    end
  end
  
  def update
    if @user.update(user_params)
      redirect_to @user, notice: 'User was successfully updated.'
    else
      render :edit
    end
  end
  
  def destroy
    @user.destroy
    redirect_to users_url, notice: 'User was successfully deleted.'
  end
  
  private
  
  def set_user
    @user = User.find(params[:id])
  end
  
  def user_params
    params.require(:user).permit(:name, :email, :status)
  end
end

# API コントローラー
class Api::V1::UsersController < ApiController
  before_action :set_user, only: [:show, :update, :destroy]
  
  def index
    @users = User.all
    render json: @users, each_serializer: UserSerializer
  end
  
  def show
    render json: @user, serializer: UserDetailSerializer
  end
  
  def create
    @user = User.new(user_params)
    
    if @user.save
      render json: @user, serializer: UserSerializer, status: :created
    else
      render json: { errors: @user.errors }, status: :unprocessable_entity
    end
  end
  
  private
  
  def set_user
    @user = User.find(params[:id])
  end
  
  def user_params
    params.require(:user).permit(:name, :email)
  end
end

# バックグラウンドジョブ
class UserDataProcessingJob < ApplicationJob
  queue_as :default
  
  def perform(user_id)
    user = User.find(user_id)
    
    # 重い処理をバックグラウンドで実行
    user.process_analytics_data
    user.update(processed_at: Time.current)
  end
end

# ジョブエンキュー
UserDataProcessingJob.perform_later(user.id)

# テスト(RSpec)
RSpec.describe User, type: :model do
  describe 'validations' do
    it { should validate_presence_of(:name) }
    it { should validate_presence_of(:email) }
    it { should validate_uniqueness_of(:email) }
  end
  
  describe 'associations' do
    it { should have_many(:posts).dependent(:destroy) }
  end
  
  describe '#full_name' do
    let(:user) { build(:user, first_name: 'John', last_name: 'Doe') }
    
    it 'returns the full name' do
      expect(user.full_name).to eq('John Doe')
    end
  end
end

# ファクトリー(FactoryBot)
FactoryBot.define do
  factory :user do
    name { Faker::Name.name }
    email { Faker::Internet.unique.email }
    status { 'active' }
    
    trait :with_posts do
      after(:create) do |user|
        create_list(:post, 3, user: user)
      end
    end
  end
  
  factory :post do
    title { Faker::Lorem.sentence(word_count: 4) }
    content { Faker::Lorem.paragraph(sentence_count: 5) }
    association :user
    published_at { 1.day.ago }
  end
end

# シード
# db/seeds.rb
User.create!([
  {
    name: 'Admin User',
    email: '[email protected]',
    status: 'active'
  },
  {
    name: 'Test User',
    email: '[email protected]',
    status: 'active'
  }
])

# 本番環境でのパフォーマンス監視
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  
  # スロークエリログ
  around_action :log_slow_queries
  
  private
  
  def log_slow_queries
    start_time = Time.current
    yield
    duration = Time.current - start_time
    
    if duration > 1.second
      Rails.logger.warn "Slow query detected: #{duration}s"
    end
  end
end