CrackedRuby logo

CrackedRuby

ActiveRecord

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