Claude
Skills
Sign in
Back

activerecord

Included with Lifetime
$97 forever

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.

General

What this skill does


# 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

```ruby
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

| 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

```ruby
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

```ruby
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

```ruby
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

```ruby
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

| 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

```ruby
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

```ruby
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

```ruby
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

```ruby
# Single record
User.find(1)                        # Raises RecordNotFound if missing
User.find_by(email: "[email protected]")      # Returns nil if missing
User.find_by!(email: "[email protected]")     # 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

```ruby
# pluck - returns array of values (no model instantiation)
User.pluck(:email)                    # ["[email protected]", "[email protected]"]
User.pluck(:id, :email)               # [[1, "[email protected]"], [2, "[email protected]"]]
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: "[email protected]")        # 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)
```

#

Related in General