Overview
ActiveRecord implements the Active Record pattern, mapping database rows to Ruby objects and encapsulating database access logic within model classes. Ruby classes inherit from ActiveRecord::Base
to gain database functionality, establishing connections between object attributes and table columns automatically.
The library handles database connections, query generation, and result mapping transparently. Models define relationships through associations, validate data integrity through built-in validators, and provide lifecycle hooks through callbacks. ActiveRecord supports multiple database adapters including PostgreSQL, MySQL, SQLite, and others.
class User < ActiveRecord::Base
has_many :posts
validates :email, presence: true, uniqueness: true
end
# Creates table mapping and accessor methods
user = User.create(email: "user@example.com", name: "John")
# => #<User id: 1, email: "user@example.com", name: "John">
Database migrations manage schema changes through Ruby code, maintaining version control over database structure. ActiveRecord generates SQL queries from Ruby method calls, abstracting database-specific syntax differences.
# Migration example
class CreateUsers < ActiveRecord::Migration[7.0]
def change
create_table :users do |t|
t.string :email, null: false, index: { unique: true }
t.string :name
t.timestamps
end
end
end
Basic Usage
Model classes inherit from ActiveRecord::Base
and automatically map to database tables using naming conventions. Table names use pluralized, snake_case versions of class names, while column names map directly to Ruby attribute names.
class BlogPost < ActiveRecord::Base
# Maps to 'blog_posts' table automatically
validates :title, :body, presence: true
belongs_to :author, class_name: 'User'
end
# Create new records
post = BlogPost.new(title: "Ruby Guide", body: "Content here")
post.author = User.find(1)
post.save!
Query methods provide chainable interfaces for database operations. where
, order
, limit
, and joins
methods build SQL queries incrementally without executing until results are accessed.
# Chainable query building
recent_posts = BlogPost.where(published: true)
.order(created_at: :desc)
.limit(10)
.includes(:author)
# SQL executes when accessing results
recent_posts.each { |post| puts post.title }
Associations define relationships between models using belongs_to
, has_many
, has_one
, and has_and_belongs_to_many
declarations. These methods generate instance methods for accessing related objects and configuring join behavior.
class User < ActiveRecord::Base
has_many :blog_posts, dependent: :destroy
has_one :profile, dependent: :destroy
has_many :comments, through: :blog_posts
end
class Profile < ActiveRecord::Base
belongs_to :user
validates :user_id, presence: true, uniqueness: true
end
Record persistence uses save
, create
, update
, and destroy
methods. These methods return boolean values or raise exceptions depending on validation results and database constraints.
# Different persistence approaches
user = User.new(email: "test@example.com")
user.save # Returns false if invalid
User.create!(email: "valid@example.com") # Raises exception if invalid
existing_user = User.find(1)
existing_user.update(name: "Updated Name") # Returns boolean
existing_user.destroy # Removes from database
Advanced Usage
Scopes define reusable query fragments as class methods, accepting parameters and chaining with other ActiveRecord methods. Named scopes improve code readability and maintain query logic within model classes.
class BlogPost < ActiveRecord::Base
scope :published, -> { where(published: true) }
scope :by_author, ->(author) { where(author: author) }
scope :recent, ->(days = 7) { where('created_at > ?', days.days.ago) }
# Chainable scope usage
def self.trending
published.recent(14).joins(:comments).group('blog_posts.id')
.having('COUNT(comments.id) > 5')
.order('COUNT(comments.id) DESC')
end
end
Custom finder methods encapsulate complex queries and provide domain-specific interfaces. These methods combine multiple conditions, joins, and aggregations while maintaining readable method names.
class User < ActiveRecord::Base
def self.active_authors_with_stats
joins(:blog_posts)
.where(blog_posts: { published: true })
.group('users.id')
.select('users.*, COUNT(blog_posts.id) as post_count')
.having('COUNT(blog_posts.id) >= ?', 5)
.order('post_count DESC')
end
def self.find_by_email_domain(domain)
where('email LIKE ?', "%@#{domain}")
end
end
Callbacks execute code at specific points in the object lifecycle, including before_save
, after_create
, before_destroy
, and others. Multiple callbacks of the same type execute in definition order, with the ability to halt execution chains.
class BlogPost < ActiveRecord::Base
before_validation :normalize_title
before_save :set_published_at
after_create :notify_subscribers
after_destroy :cleanup_associated_files
private
def normalize_title
self.title = title.strip.titleize if title.present?
end
def set_published_at
self.published_at = Time.current if published? && published_at.nil?
end
def notify_subscribers
NotificationJob.perform_later(self)
end
end
Complex associations support advanced relationship configurations including polymorphic associations, self-referential relationships, and custom foreign keys. These patterns handle sophisticated domain models with flexible relationship structures.
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
belongs_to :parent, class_name: 'Comment', optional: true
has_many :replies, class_name: 'Comment', foreign_key: 'parent_id'
scope :top_level, -> { where(parent_id: nil) }
scope :for_type, ->(type) { where(commentable_type: type) }
end
class BlogPost < ActiveRecord::Base
has_many :comments, as: :commentable, dependent: :destroy
has_many :top_level_comments, -> { top_level },
as: :commentable, class_name: 'Comment'
end
Performance & Memory
Query optimization focuses on reducing database round trips through eager loading and strategic use of includes
, preload
, and joins
. The N+1 query problem occurs when iterating through records that access associated data individually.
# N+1 problem - executes query for each post
posts = BlogPost.limit(10)
posts.each { |post| puts post.author.name } # 11 queries total
# Solution: eager loading
posts = BlogPost.includes(:author).limit(10)
posts.each { |post| puts post.author.name } # 2 queries total
# Complex eager loading with conditions
BlogPost.includes(author: :profile, comments: :user)
.where(published: true)
.where(authors: { active: true })
Database indexing significantly impacts query performance, especially for columns used in WHERE
, ORDER BY
, and JOIN
clauses. Composite indexes handle multi-column queries more efficiently than individual column indexes.
# Migration with strategic indexing
class AddIndexesToOptimizeQueries < ActiveRecord::Migration[7.0]
def change
add_index :blog_posts, [:published, :created_at] # Composite index
add_index :blog_posts, :author_id # Foreign key index
add_index :users, :email, unique: true # Unique constraint
add_index :comments, [:commentable_type, :commentable_id] # Polymorphic
end
end
Select optimization reduces memory usage by loading only required columns instead of entire records. This approach particularly benefits queries processing large datasets or accessing few attributes from wide tables.
# Memory-efficient column selection
user_emails = User.where(active: true).pluck(:email) # Returns array
user_data = User.select(:id, :email, :name).where(active: true)
# Batch processing for large datasets
User.where(created_at: 1.year.ago..Time.current).find_in_batches do |batch|
batch.each { |user| ProcessUserJob.perform_later(user) }
end
# Count optimization
User.joins(:blog_posts).group('users.id').count # Avoids loading records
Query analysis tools help identify performance bottlenecks through SQL examination and execution plan review. ActiveRecord provides logging and explain plan integration for performance debugging.
# Query analysis in development
ActiveRecord::Base.logger = Logger.new(STDOUT)
# Explain plans for complex queries
puts User.joins(:blog_posts).where(blog_posts: { published: true }).explain
# Custom query optimization
users_with_post_counts = User.joins(
'LEFT JOIN blog_posts ON blog_posts.author_id = users.id'
).select(
'users.*, COALESCE(COUNT(blog_posts.id), 0) as posts_count'
).group('users.id')
Production Patterns
Database connection pooling manages concurrent access to database resources through configurable pool sizes and timeout settings. Connection pools prevent resource exhaustion while maintaining reasonable response times under load.
# Database configuration for production
production:
adapter: postgresql
pool: 25
timeout: 5000
checkout_timeout: 5
reaping_frequency: 10
dead_connection_timeout: 30
Read replica configuration distributes database load by routing SELECT queries to replica servers while maintaining write operations on the primary database. This pattern scales read-heavy applications effectively.
class User < ActiveRecord::Base
connects_to database: { writing: :primary, reading: :replica }
def self.search_active_users(term)
# Automatically routes to read replica
where(active: true).where('name ILIKE ?', "%#{term}%")
end
end
# Explicit replica routing
ActiveRecord::Base.connected_to(role: :reading) do
User.where(active: true).count
end
Database migrations in production environments require careful coordination to avoid downtime and data loss. Zero-downtime migrations use backwards-compatible changes and feature flags to safely deploy schema updates.
# Safe migration patterns
class AddEmailIndexSafely < ActiveRecord::Migration[7.0]
disable_ddl_transaction! # Required for concurrent index creation
def up
add_index :users, :email, algorithm: :concurrently
end
def down
remove_index :users, :email
end
end
# Data migration with batching
class MigrateUserEmailFormat < ActiveRecord::Migration[7.0]
def up
User.where('email NOT LIKE ?', '%@%').find_in_batches do |batch|
batch.each do |user|
user.update_column(:email, "#{user.email}@example.com")
end
end
end
end
Monitoring and logging capture database performance metrics, slow query identification, and error tracking. Application Performance Monitoring (APM) tools integrate with ActiveRecord to provide query-level visibility.
# Custom query monitoring
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
# Log slow queries
def self.log_slow_queries
threshold = 100 # milliseconds
ActiveSupport::Notifications.subscribe 'sql.active_record' do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
if event.duration > threshold
Rails.logger.warn "Slow Query (#{event.duration.round(2)}ms): #{event.payload[:sql]}"
end
end
end
end
Reference
Core Classes and Modules
Class/Module | Purpose | Key Methods |
---|---|---|
ActiveRecord::Base |
Base class for models | save , create , find , where , update , destroy |
ActiveRecord::Migration |
Database schema changes | create_table , add_column , add_index , change |
ActiveRecord::Relation |
Query result collection | where , order , limit , joins , includes , group |
ActiveRecord::Associations |
Relationship definitions | belongs_to , has_many , has_one , has_and_belongs_to_many |
Query Methods
Method | Parameters | Returns | Description |
---|---|---|---|
where(conditions) |
Hash, String, Array | Relation |
Filters records by conditions |
order(column) |
String, Symbol, Hash | Relation |
Sorts results by column(s) |
limit(count) |
Integer | Relation |
Limits result count |
offset(count) |
Integer | Relation |
Skips initial records |
joins(table) |
String, Symbol, Hash | Relation |
SQL JOIN operations |
includes(association) |
Symbol, Array, Hash | Relation |
Eager loads associations |
group(column) |
String, Symbol | Relation |
Groups results for aggregation |
having(conditions) |
String, Hash | Relation |
Filters grouped results |
Persistence Methods
Method | Parameters | Returns | Description |
---|---|---|---|
save |
None | Boolean |
Persists record, returns success |
save! |
None | self or Exception |
Persists record, raises on failure |
create(attributes) |
Hash | Model |
Creates and saves new record |
create!(attributes) |
Hash | Model or Exception |
Creates record, raises on failure |
update(attributes) |
Hash | Boolean |
Updates attributes and saves |
update!(attributes) |
Hash | self or Exception |
Updates attributes, raises on failure |
destroy |
None | self |
Removes record from database |
Association Options
Option | Type | Purpose | Example |
---|---|---|---|
dependent |
Symbol | Deletion behavior | :destroy , :delete_all , :nullify |
foreign_key |
String/Symbol | Custom foreign key | foreign_key: 'owner_id' |
primary_key |
String/Symbol | Custom primary key | primary_key: 'uuid' |
class_name |
String | Custom model class | class_name: 'Person' |
inverse_of |
Symbol | Bidirectional association | inverse_of: :user |
through |
Symbol | Join through association | through: :memberships |
Validation Methods
Method | Parameters | Returns | Description |
---|---|---|---|
validates :attr, presence: true |
Symbol, Hash | None | Requires attribute presence |
validates :attr, uniqueness: true |
Symbol, Hash | None | Ensures attribute uniqueness |
validates :attr, length: { minimum: 5 } |
Symbol, Hash | None | Validates string length |
validates :attr, format: { with: /regex/ } |
Symbol, Hash | None | Validates format pattern |
validates :attr, numericality: true |
Symbol, Hash | None | Validates numeric values |
Callback Types
Callback | Timing | Use Cases |
---|---|---|
before_validation |
Before validation runs | Data normalization |
after_validation |
After validation passes | Complex validation setup |
before_save |
Before database save | Calculated field updates |
after_save |
After database save | External service notifications |
before_create |
Before initial save | Default value assignment |
after_create |
After initial save | Welcome email sending |
before_update |
Before existing record save | Change tracking |
after_update |
After existing record save | Cache invalidation |
before_destroy |
Before record deletion | Dependency cleanup |
after_destroy |
After record deletion | Audit log entries |
Common Exceptions
Exception | Cause | Handling Strategy |
---|---|---|
ActiveRecord::RecordNotFound |
find with invalid ID |
Use find_by or rescue |
ActiveRecord::RecordInvalid |
Validation failure with ! methods |
Check errors object |
ActiveRecord::RecordNotUnique |
Unique constraint violation | Handle duplicate data |
ActiveRecord::ConnectionTimeoutError |
Connection pool exhaustion | Increase pool size or timeout |
ActiveRecord::StatementInvalid |
SQL syntax or constraint error | Review query and constraints |