Active Record
ActiveRecord query patterns, associations, validations, callbacks, and performance optimization for Rails applications.
You are an expert in ActiveRecord ORM patterns for building performant, maintainable Rails applications. ## Key Points - Use `find_each` or `in_batches` for processing large datasets instead of `.all.each`. - Prefer `select` to limit columns when you do not need full objects. - Use `exists?` instead of `present?` for existence checks -- it generates a more efficient query. - Add database-level constraints (NOT NULL, foreign keys, unique indexes) in addition to model validations. - Use `transaction` blocks when multiple records must be saved atomically. - Favor `after_commit` over `after_save` for side effects like sending emails or enqueueing jobs. - Keep callbacks thin; delegate heavy work to service objects or jobs. - Index foreign keys and columns used in `WHERE`, `ORDER BY`, and `GROUP BY` clauses. - **N+1 queries**: Always check logs or use the `bullet` gem to detect eager loading issues. - **Callback chains**: Excessive callbacks make models unpredictable. Extract logic into service objects. - **Ignoring `dependent` options**: Forgetting `dependent: :destroy` or `:nullify` leads to orphaned records. - **Using `default_scope`**: It applies everywhere and is hard to override. Prefer explicit named scopes.
skilldb get ruby-rails-skills/Active RecordFull skill: 260 linesActiveRecord Patterns — Ruby on Rails
You are an expert in ActiveRecord ORM patterns for building performant, maintainable Rails applications.
Overview
ActiveRecord is the ORM layer in Rails that maps database tables to Ruby classes. Mastering its query interface, association strategies, and performance characteristics is essential for any Rails application that deals with relational data.
Core Philosophy
ActiveRecord embodies the principle that database access should feel natural in Ruby. Rather than treating SQL as a separate concern to be managed through raw strings and manual mapping, ActiveRecord lets you express data relationships and queries using Ruby's own idioms. The goal is to make the 80% of database work trivially simple while still providing escape hatches for the remaining 20%.
Convention over configuration is central to how ActiveRecord operates. Table names, primary keys, foreign keys, and timestamps all follow predictable defaults that eliminate boilerplate. When you follow these conventions, you get powerful features like automatic association inference, schema-aware form builders, and migration generators essentially for free. Deviating from conventions is always possible but should be a conscious decision.
Performance awareness must be baked into your ActiveRecord usage from day one, not bolted on later. Understanding when Rails fires queries, how eager loading prevents N+1 problems, and why database-level constraints matter more than model-only validations separates sustainable Rails codebases from ones that collapse under real traffic.
Anti-Patterns
-
The God Model: Stuffing hundreds of methods, callbacks, and validations into a single model (often
User) until it becomes impossible to understand or test. Extract domain logic into service objects, query objects, and focused concerns. -
Callback-Driven Architecture: Chaining
after_save,after_commit, andbefore_validationcallbacks to orchestrate complex business workflows. This creates invisible execution paths that are difficult to debug, test, and reason about. Use explicit service objects for multi-step operations. -
Scope Creep on Scopes: Defining dozens of one-off scopes that are each used in only one place. Scopes should represent reusable, composable query fragments. If a query is used once, inline it or put it in a query object.
-
Validations Without Database Constraints: Relying solely on ActiveRecord validations for uniqueness, presence, or foreign key integrity. Model validations are subject to race conditions. Always back critical validations with database-level constraints (unique indexes, NOT NULL, foreign keys).
-
Raw SQL Everywhere: Bypassing the query interface with
find_by_sqlor interpolatedwherestrings for queries that ActiveRecord handles natively. This sacrifices composability, parameterization safety, and readability. Reserve raw SQL for genuinely complex queries that the query interface cannot express.
Core Concepts
Associations
ActiveRecord supports six types of associations that define relationships between models:
class User < ApplicationRecord
has_many :posts, dependent: :destroy
has_many :comments, through: :posts
has_one :profile, dependent: :destroy
has_and_belongs_to_many :roles
end
class Post < ApplicationRecord
belongs_to :user, counter_cache: true
has_many :comments, dependent: :destroy
has_many :taggings, dependent: :destroy
has_many :tags, through: :taggings
end
Polymorphic Associations
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end
class Post < ApplicationRecord
has_many :comments, as: :commentable
end
class Photo < ApplicationRecord
has_many :comments, as: :commentable
end
Validations
class User < ApplicationRecord
validates :email, presence: true,
uniqueness: { case_sensitive: false },
format: { with: URI::MailTo::EMAIL_REGEXP }
validates :username, presence: true,
length: { minimum: 3, maximum: 30 },
uniqueness: true
validates :age, numericality: { greater_than: 0 }, allow_nil: true
validate :custom_validation
private
def custom_validation
errors.add(:base, "Account is suspended") if suspended? && active_changed?
end
end
Scopes and Query Interface
class Post < ApplicationRecord
scope :published, -> { where(published: true) }
scope :recent, -> { order(created_at: :desc) }
scope :by_author, ->(user_id) { where(user_id: user_id) }
scope :with_comments, -> { joins(:comments).distinct }
scope :popular, -> { where("views_count > ?", 100) }
# Composable scopes
scope :trending, -> { published.recent.popular }
end
# Usage
Post.published.recent.limit(10)
Post.where(category: "tech").or(Post.where(category: "science"))
Post.where.not(status: "draft")
Post.where(created_at: 1.week.ago..)
Implementation Patterns
Eager Loading to Avoid N+1 Queries
# Bad: N+1 query
users = User.all
users.each { |u| puts u.posts.count }
# Good: eager load with includes
users = User.includes(:posts).all
users.each { |u| puts u.posts.size }
# Use preload when you want separate queries
users = User.preload(:posts, :profile).all
# Use eager_load when you need to filter on associations
users = User.eager_load(:posts).where(posts: { published: true })
Complex Queries with Arel
class Post < ApplicationRecord
def self.search(query)
where(
arel_table[:title].matches("%#{sanitize_sql_like(query)}%")
.or(arel_table[:body].matches("%#{sanitize_sql_like(query)}%"))
)
end
def self.with_recent_comments
joins(:comments)
.where(comments: { created_at: 1.day.ago.. })
.distinct
end
end
Callbacks (Use Sparingly)
class Order < ApplicationRecord
before_validation :normalize_card_number, on: :create
after_create :send_confirmation_email
after_commit :update_inventory, on: :create
private
def normalize_card_number
self.card_number = card_number.gsub(/\s+/, "")
end
def send_confirmation_email
OrderMailer.confirmation(self).deliver_later
end
def update_inventory
UpdateInventoryJob.perform_later(id)
end
end
Migrations
class CreateProducts < ActiveRecord::Migration[7.1]
def change
create_table :products do |t|
t.string :name, null: false
t.text :description
t.decimal :price, precision: 10, scale: 2, null: false
t.references :category, null: false, foreign_key: true
t.integer :stock_count, default: 0
t.jsonb :metadata, default: {}
t.timestamps
end
add_index :products, :name
add_index :products, :metadata, using: :gin
end
end
Query Objects for Complex Logic
class UserSearchQuery
def initialize(relation = User.all)
@relation = relation
end
def call(params)
result = @relation
result = filter_by_status(result, params[:status])
result = filter_by_role(result, params[:role])
result = filter_by_date_range(result, params[:from], params[:to])
result = sort(result, params[:sort_by], params[:direction])
result
end
private
def filter_by_status(relation, status)
return relation if status.blank?
relation.where(status: status)
end
def filter_by_role(relation, role)
return relation if role.blank?
relation.joins(:roles).where(roles: { name: role })
end
def filter_by_date_range(relation, from, to)
relation = relation.where(created_at: from..) if from.present?
relation = relation.where(created_at: ..to) if to.present?
relation
end
def sort(relation, column, direction)
return relation.order(created_at: :desc) if column.blank?
relation.order(column => direction || :asc)
end
end
Best Practices
- Use
find_eachorin_batchesfor processing large datasets instead of.all.each. - Prefer
selectto limit columns when you do not need full objects. - Use
exists?instead ofpresent?for existence checks -- it generates a more efficient query. - Add database-level constraints (NOT NULL, foreign keys, unique indexes) in addition to model validations.
- Use
transactionblocks when multiple records must be saved atomically. - Favor
after_commitoverafter_savefor side effects like sending emails or enqueueing jobs. - Keep callbacks thin; delegate heavy work to service objects or jobs.
- Index foreign keys and columns used in
WHERE,ORDER BY, andGROUP BYclauses.
Common Pitfalls
- N+1 queries: Always check logs or use the
bulletgem to detect eager loading issues. - Callback chains: Excessive callbacks make models unpredictable. Extract logic into service objects.
- Ignoring
dependentoptions: Forgettingdependent: :destroyor:nullifyleads to orphaned records. - Using
default_scope: It applies everywhere and is hard to override. Prefer explicit named scopes. - Large transactions: Wrapping too many operations in a single transaction can lock tables and hurt concurrency.
- Skipping database constraints: Relying solely on model validations can lead to data integrity issues under race conditions.
Install this skill directly: skilldb add ruby-rails-skills
Related Skills
API Mode
Building JSON APIs with Rails API mode, serialization, versioning, authentication, and rate limiting.
Concerns Modules
ActiveSupport::Concern patterns, module design, and code organization strategies for maintainable Rails applications.
Deployment
Deploying Rails applications with Kamal, Docker, and production best practices for infrastructure and operations.
Hotwire Turbo
Hotwire and Turbo Drive, Frames, and Streams for building reactive Rails frontends without heavy JavaScript.
Sidekiq
Background job processing with Sidekiq, including job design, error handling, queues, and performance tuning in Rails.
Stimulus
Stimulus.js controller patterns for adding interactive behavior to server-rendered Rails HTML.