| name | activerecord |
| description | This skill should be used when the user asks about "ActiveRecord", "database queries", "associations", "validations", "migrations", "scopes", "callbacks", "N+1 queries", "eager loading", "includes", "joins", "eager_load", "preload", "database optimization", "model relationships", "has_many", "belongs_to", "has_one", "polymorphic associations", "pluck", "exists", or needs guidance on database-related Rails topics. |
| version | 1.1.0 |
ActiveRecord
Comprehensive guide to ActiveRecord associations, queries, validations, and database optimization in Rails.
Associations
Association Types
| Type | Description |
|---|
belongs_to | Foreign key on this model's table |
has_one | Foreign key on other model's table (singular) |
has_many | Foreign key on other model's table (plural) |
has_many :through | Many-to-many via join model |
has_one :through | One-to-one via join model |
has_and_belongs_to_many | Many-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
has_many :comments, dependent: :destroy
has_many :taggings, dependent: :destroy
has_many :tags, through: :taggings
end
Association Options
| Option | Purpose |
|---|
dependent: :destroy | Delete associated records via callbacks |
dependent: :delete_all | Delete directly via SQL (no callbacks) |
dependent: :nullify | Set foreign key to NULL |
dependent: :restrict_with_error | Add error if associated records exist |
dependent: :restrict_with_exception | Raise exception if associated |
optional: true | Allow nil belongs_to (required by default in Rails 5+) |
inverse_of | Specify inverse association for bidirectional optimization |
counter_cache: true | Maintain count column automatically |
touch: true | Update parent's updated_at on changes |
class_name | Specify associated class when name differs |
foreign_key | Specify 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
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
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
validates :name, presence: true
validates :spam_flag, absence: true
validates :terms_of_service, acceptance: true
validates :email, confirmation: true
validates :email, uniqueness: { case_sensitive: false, scope: :account_id }
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
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 }
validates :age, numericality: { only_integer: true, greater_than: 0 }
validates :price, numericality: { greater_than_or_equal_to: 0 }
validates :role, inclusion: { in: %w[admin editor viewer] }
validates :subdomain, exclusion: { in: %w[www admin api] }
validates :end_date, comparison: { greater_than: :start_date }
validates :age, comparison: { greater_than_or_equal_to: 18 }
validates_associated :profile
end
Common Validation Options
| Option | Purpose |
|---|
:message | Custom error message (supports %{value}, %{attribute}, %{model}) |
:on | When to validate: :create, :update, or custom context |
:allow_nil | Skip validation if value is nil |
:allow_blank | Skip validation if value is blank |
:if / :unless | Conditional validation (symbol, proc, or array) |
:strict | Raise 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?
validates :coupon_code, presence: true, if: -> { discount_percentage.present? }
validates :phone, presence: true, if: [:contact_by_phone?, :phone_required?]
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
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) }
scope :by_author, ->(author) { where(author: author) }
scope :created_after, ->(date) { where(created_at: date..) }
scope :limit_recent, ->(count = 10) { recent.limit(count) }
scope :featured, -> { published.where(featured: true).recent }
end
Article.published.by_author(user).recent.limit(5)
Queries
Finding Records
User.find(1)
User.find_by(email: "a@b.com")
User.find_by!(email: "a@b.com")
User.first
User.last
User.take
User.where(status: "active")
User.where.not(role: "admin")
User.where(created_at: 1.week.ago..)
User.where(age: 18..65)
User.where(role: "admin").or(User.where(role: "editor"))
User.select(:id, :email, :name)
User.distinct
Efficient Data Extraction
User.pluck(:email)
User.pluck(:id, :email)
User.where(active: true).pluck(:id)
User.where(active: true).ids
User.exists?(email: "a@b.com")
User.where(role: "admin").exists?
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)"))
User.limit(10)
User.offset(20).limit(10)
Complex Conditions
User.where("created_at > ? AND role = ?", 1.week.ago, "admin")
User.where("name LIKE :q OR email LIKE :q", q: "%#{search}%")
active_ids = User.where(active: true).select(:id)
Article.where(user_id: active_ids)
Order.group(:status).count
Order.group(:user_id).having("COUNT(*) > 5")
Eager Loading (N+1 Prevention)
The Problem
articles = Article.limit(10)
articles.each { |a| puts a.author.name }
articles = Article.includes(:author).limit(10)
articles.each { |a| puts a.author.name }
Methods Comparison
| Method | Strategy | Use When |
|---|
includes | Rails decides (2 queries OR JOIN) | General use |
preload | Always separate queries | Large datasets, simpler queries |
eager_load | Always LEFT OUTER JOIN | Need to filter/order by association |
Article.includes(:author, :tags)
Article.includes(comments: :user)
Article.preload(:author, :comments)
Article.eager_load(:author).where(users: { role: "admin" })
Article.joins(:author).where(users: { active: true })
Callbacks
class Article < ApplicationRecord
before_validation :normalize_title
before_save :sanitize_content
after_create :notify_subscribers
after_commit :update_search_index, on: :create
private
def notify_subscribers
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
class AddIndexToArticles < ActiveRecord::Migration[7.1]
disable_ddl_transaction!
def change
add_index :articles, :title, algorithm: :concurrently
end
end
Batch Processing
User.find_each(batch_size: 1000) { |user| process(user) }
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