Back to list
bastos

activerecord

by bastos

Bastos' Claude Code Ruby Plugin Marketplace

0🍴 0📅 Jan 24, 2026

SKILL.md


ActiveRecord

Comprehensive guide to ActiveRecord associations, queries, validations, and database optimization in Rails.

Associations

Association Types

TypeDescription
belongs_toForeign key on this model's table
has_oneForeign key on other model's table (singular)
has_manyForeign key on other model's table (plural)
has_many :throughMany-to-many via join model
has_one :throughOne-to-one via join model
has_and_belongs_to_manyMany-to-many via join table (no model)

Basic Associations

class User < ApplicationRecord
  has_one :profile, dependent: :destroy
  has_many :articles, dependent: :destroy
  has_many :comments, dependent: :destroy
end

class Article < ApplicationRecord
  belongs_to :user
  belongs_to :category, optional: true  # Allow nil
  has_many :comments, dependent: :destroy
  has_many :taggings, dependent: :destroy
  has_many :tags, through: :taggings
end

Association Options

OptionPurpose
dependent: :destroyDelete associated records via callbacks
dependent: :delete_allDelete directly via SQL (no callbacks)
dependent: :nullifySet foreign key to NULL
dependent: :restrict_with_errorAdd error if associated records exist
dependent: :restrict_with_exceptionRaise exception if associated
optional: trueAllow nil belongs_to (required by default in Rails 5+)
inverse_ofSpecify inverse association for bidirectional optimization
counter_cache: trueMaintain count column automatically
touch: trueUpdate parent's updated_at on changes
class_nameSpecify associated class when name differs
foreign_keySpecify custom foreign key column

Has Many Through

class Doctor < ApplicationRecord
  has_many :appointments
  has_many :patients, through: :appointments
end

class Patient < ApplicationRecord
  has_many :appointments
  has_many :doctors, through: :appointments
end

class Appointment < ApplicationRecord
  belongs_to :doctor
  belongs_to :patient
  # Join model can have its own attributes: appointment_date, notes, etc.
end

Polymorphic Associations

class Comment < ApplicationRecord
  belongs_to :commentable, polymorphic: true
end

class Article < ApplicationRecord
  has_many :comments, as: :commentable
end

class Photo < ApplicationRecord
  has_many :comments, as: :commentable
end

# Migration
create_table :comments do |t|
  t.references :commentable, polymorphic: true, index: true
  t.text :body
  t.timestamps
end

Self-Referential

class Employee < ApplicationRecord
  belongs_to :manager, class_name: "Employee", optional: true
  has_many :subordinates, class_name: "Employee", foreign_key: "manager_id"
end

Validations

Built-in Validation Helpers

class User < ApplicationRecord
  # Presence - not empty (uses Object#blank?)
  validates :name, presence: true

  # Absence - must be blank
  validates :spam_flag, absence: true

  # Acceptance - checkbox must be checked
  validates :terms_of_service, acceptance: true

  # Confirmation - two fields must match
  validates :email, confirmation: true
  # Requires email_confirmation field in form

  # Uniqueness (case-insensitive)
  validates :email, uniqueness: { case_sensitive: false, scope: :account_id }

  # Format - regex match
  validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }

  # Length
  validates :password, length: { minimum: 8, maximum: 72 }
  validates :bio, length: { maximum: 500, too_long: "%{count} characters max" }
  validates :code, length: { is: 6 }
  validates :name, length: { in: 2..50 }

  # Numericality
  validates :age, numericality: { only_integer: true, greater_than: 0 }
  validates :price, numericality: { greater_than_or_equal_to: 0 }

  # Inclusion - value must be in list
  validates :role, inclusion: { in: %w[admin editor viewer] }

  # Exclusion - value must NOT be in list
  validates :subdomain, exclusion: { in: %w[www admin api] }

  # Comparison - compare to another attribute or value
  validates :end_date, comparison: { greater_than: :start_date }
  validates :age, comparison: { greater_than_or_equal_to: 18 }

  # Associated records must also be valid
  validates_associated :profile
end

Common Validation Options

OptionPurpose
:messageCustom error message (supports %{value}, %{attribute}, %{model})
:onWhen to validate: :create, :update, or custom context
:allow_nilSkip validation if value is nil
:allow_blankSkip validation if value is blank
:if / :unlessConditional validation (symbol, proc, or array)
:strictRaise ActiveModel::StrictValidationFailed instead of adding error

Conditional Validations

class Order < ApplicationRecord
  validates :shipping_address, presence: true, if: :requires_shipping?
  validates :credit_card, presence: true, unless: :free_order?

  # Proc
  validates :coupon_code, presence: true, if: -> { discount_percentage.present? }

  # Multiple conditions (all :if must pass AND none of :unless)
  validates :phone, presence: true, if: [:contact_by_phone?, :phone_required?]

  # Group validations
  with_options if: :premium_user? do
    validates :credit_card, presence: true
    validates :billing_address, presence: true
  end
end

Custom Validations

class User < ApplicationRecord
  validate :email_domain_allowed
  validates_with EmailValidator

  private

  def email_domain_allowed
    return if email.blank?
    domain = email.split("@").last
    errors.add(:email, "must be from an allowed domain") unless allowed_domain?(domain)
  end
end

# Custom validator class
class EmailValidator < ActiveModel::Validator
  def validate(record)
    unless record.email.include?("@")
      record.errors.add(:email, "must contain @")
    end
  end
end

Scopes

class Article < ApplicationRecord
  scope :published, -> { where(status: "published") }
  scope :draft, -> { where(status: "draft") }
  scope :recent, -> { order(created_at: :desc) }

  # With arguments
  scope :by_author, ->(author) { where(author: author) }
  scope :created_after, ->(date) { where(created_at: date..) }

  # With defaults
  scope :limit_recent, ->(count = 10) { recent.limit(count) }

  # Combining
  scope :featured, -> { published.where(featured: true).recent }
end

# Chainable
Article.published.by_author(user).recent.limit(5)

Queries

Finding Records

# Single record
User.find(1)                        # Raises RecordNotFound if missing
User.find_by(email: "a@b.com")      # Returns nil if missing
User.find_by!(email: "a@b.com")     # Raises if missing
User.first                          # First by primary key
User.last                           # Last by primary key
User.take                           # Any record (no ordering)

# Collections
User.where(status: "active")
User.where.not(role: "admin")
User.where(created_at: 1.week.ago..)  # Range (>= 1 week ago)
User.where(age: 18..65)               # BETWEEN

# OR conditions
User.where(role: "admin").or(User.where(role: "editor"))

# Selection
User.select(:id, :email, :name)     # Specific columns
User.distinct                        # Remove duplicates

Efficient Data Extraction

# pluck - returns array of values (no model instantiation)
User.pluck(:email)                    # ["a@b.com", "c@d.com"]
User.pluck(:id, :email)               # [[1, "a@b.com"], [2, "c@d.com"]]
User.where(active: true).pluck(:id)

# ids - shortcut for pluck(:id)
User.where(active: true).ids          # [1, 2, 3]

# exists? - boolean check without loading records
User.exists?(email: "a@b.com")        # true/false
User.where(role: "admin").exists?     # true/false

# count/sum/average/minimum/maximum
Order.count
Order.sum(:total)
Order.average(:total)
Order.maximum(:created_at)

Ordering and Limiting

User.order(created_at: :desc)
User.order(last_name: :asc, first_name: :asc)
User.order(Arel.sql("LOWER(name)"))   # Raw SQL (use carefully)

User.limit(10)
User.offset(20).limit(10)             # Pagination

Complex Conditions

# Parameterized SQL (prevents injection)
User.where("created_at > ? AND role = ?", 1.week.ago, "admin")

# Named parameters
User.where("name LIKE :q OR email LIKE :q", q: "%#{search}%")

# Subqueries
active_ids = User.where(active: true).select(:id)
Article.where(user_id: active_ids)

# Group and having
Order.group(:status).count                    # { "pending" => 5, "shipped" => 10 }
Order.group(:user_id).having("COUNT(*) > 5")

Eager Loading (N+1 Prevention)

The Problem

# BAD: N+1 queries (1 + 10 = 11 queries)
articles = Article.limit(10)
articles.each { |a| puts a.author.name }

# GOOD: 2 queries total
articles = Article.includes(:author).limit(10)
articles.each { |a| puts a.author.name }

Methods Comparison

MethodStrategyUse When
includesRails decides (2 queries OR JOIN)General use
preloadAlways separate queriesLarge datasets, simpler queries
eager_loadAlways LEFT OUTER JOINNeed to filter/order by association
# includes - default choice
Article.includes(:author, :tags)
Article.includes(comments: :user)

# preload - separate queries
Article.preload(:author, :comments)

# eager_load - single JOIN query (required for filtering)
Article.eager_load(:author).where(users: { role: "admin" })

# joins - INNER JOIN for filtering only (doesn't load association)
Article.joins(:author).where(users: { active: true })
# Note: joins doesn't prevent N+1 if you access the association!

Callbacks

class Article < ApplicationRecord
  # Lifecycle order: validation → save → create/update → commit
  before_validation :normalize_title
  before_save :sanitize_content
  after_create :notify_subscribers
  after_commit :update_search_index, on: :create

  private

  def notify_subscribers
    # Use jobs for external operations
    NotifySubscribersJob.perform_later(id)
  end
end

Best practices:

  • Keep callbacks simple and synchronous
  • Use after_commit for external services (email, webhooks)
  • Move complex logic to service objects
  • Avoid callbacks that trigger other callbacks

Migrations

Common Operations

class CreateArticles < ActiveRecord::Migration[7.1]
  def change
    create_table :articles do |t|
      t.string :title, null: false
      t.text :body
      t.integer :view_count, default: 0
      t.boolean :published, default: false
      t.decimal :price, precision: 10, scale: 2
      t.datetime :published_at
      t.references :user, null: false, foreign_key: true
      t.timestamps
    end

    add_index :articles, :title
    add_index :articles, [:user_id, :published_at]
  end
end

Safe Migrations

# Add index concurrently (PostgreSQL, no locks)
class AddIndexToArticles < ActiveRecord::Migration[7.1]
  disable_ddl_transaction!

  def change
    add_index :articles, :title, algorithm: :concurrently
  end
end

Batch Processing

# find_each - loads in batches, yields one at a time
User.find_each(batch_size: 1000) { |user| process(user) }

# in_batches - yields batches as relations
User.in_batches(of: 1000) do |batch|
  batch.update_all(processed: true)
end

Additional Resources

Reference Files

  • references/query-optimization.md - N+1 detection, indexing strategies

Score

Total Score

65/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

Reviews

💬

Reviews coming soon