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.
/plugin marketplace add bastos/rails-plugin/plugin install bastos-ruby-on-rails@bastos/rails-pluginThis skill inherits all available tools. When active, it can use any tool Claude has access to.
references/query-optimization.mdComprehensive guide to ActiveRecord associations, queries, validations, and database optimization in Rails.
| 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) |
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
| 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 |
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
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
class Employee < ApplicationRecord
belongs_to :manager, class_name: "Employee", optional: true
has_many :subordinates, class_name: "Employee", foreign_key: "manager_id"
end
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
| 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 |
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
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
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)
# 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
# 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)
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
# 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")
# 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 }
| 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 |
# 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!
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:
after_commit for external services (email, webhooks)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
# 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
# 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
references/query-optimization.md - N+1 detection, indexing strategiesThis skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.