ActiveRecord for Rails: A Practical Complete Guide

ActiveRecord Complete Guide – Associations, Validations & Best Practices

ActiveRecord Documentation

Complete guide to Rails ORM

ActiveRecord is the Object-Relational Mapping (ORM) framework for Ruby on Rails, providing an elegant interface for database interactions.

Overview

What is ActiveRecord? ActiveRecord is the Object-Relational Mapping (ORM) layer of Ruby on Rails. Think of it as a translator between your Ruby objects and your database tables.
What Does ActiveRecord Do?

ActiveRecord automatically connects your Ruby classes to database tables, so you can work with database records as if they were regular Ruby objects. Instead of writing raw SQL, you write Ruby code.

Before ActiveRecord (Raw SQL)

# Without ActiveRecord - you'd write SQL like this:
sql = "SELECT * FROM users WHERE email = '[email protected]'"
result = database.execute(sql)
user_data = result.first
user = User.new(user_data)

With ActiveRecord (Ruby Code)

# With ActiveRecord - simple Ruby code:
user = User.find_by(email: '[email protected]')
# That's it! ActiveRecord handles the SQL for you.
Key Benefits
  • No SQL Required: Write Ruby code instead of SQL queries
  • Database Agnostic: Works with MySQL, PostgreSQL, SQLite, and more
  • Automatic Mapping: Tables become classes, columns become attributes
  • Built-in Security: Protects against SQL injection attacks
  • Convention over Configuration: Follow Rails conventions for less setup
How It Works – Simple Example

Let’s say you have a database table called users with columns id, name, email, and created_at.

# Your database table: users
# | id | name  | email           | created_at |
# |----|-------|-----------------|------------|
# | 1  | John  | [email protected]  | 2024-01-01 |
# | 2  | Jane  | [email protected]  | 2024-01-02 |

# ActiveRecord automatically creates a User class:
class User < ApplicationRecord
  # ActiveRecord automatically maps:
  # - table 'users' → class User
  # - column 'name' → attribute @name
  # - column 'email' → attribute @email
  # - etc.
end

# Now you can work with users like Ruby objects:
user = User.find(1)           # Find user with ID 1
puts user.name                 # "John"
puts user.email                # "[email protected]"

# Create a new user:
new_user = User.create(
  name: "Alice",
  email: "[email protected]"
)
Convention Over Configuration

ActiveRecord follows Rails conventions, which means less configuration and more productivity:

DatabaseRuby/RailsExample
Table nameModel class name (singular)users table → User class
Primary keyAlways named 'id'id column
Foreign keysEnd with '_id'user_id, post_id
TimestampsAutomatic created_at/updated_atcreated_at, updated_at
Common ActiveRecord Operations

Here are the most common things you'll do with ActiveRecord:

Create Records

# Create and save immediately
user = User.create(name: "John", email: "[email protected]")

# Create without saving
user = User.new(name: "John", email: "[email protected]")
user.save  # Now it's saved to database

Find Records

# Find by ID
user = User.find(1)

# Find by attribute
user = User.find_by(email: "[email protected]")

# Find all records
all_users = User.all

# Find with conditions
active_users = User.where(active: true)

Update Records

# Update a single record
user = User.find(1)
user.name = "Johnny"
user.save

# Update multiple records
User.where(active: false).update_all(active: true)

Delete Records

# Delete a single record
user = User.find(1)
user.destroy

# Delete multiple records
User.where(active: false).destroy_all
Why Use ActiveRecord?
  • Productivity: Write less code, get more done
  • Readability: Ruby code is easier to read than SQL
  • Maintainability: Changes to database structure are easier to manage
  • Security: Built-in protection against common attacks
  • Testing: Easier to test database operations
Pro Tip: ActiveRecord is not just for Rails! You can use it in any Ruby application by including the ActiveRecord gem and configuring your database connection.

Models

Models are Ruby classes that are enhanced with ActiveRecord capabilities. They represent database tables and provide an interface for interacting with the data. Think of models as the "smart" objects that know how to talk to your database.

Model = Class + Database Table Every model corresponds to a database table, and every instance of a model represents a row in that table.
Basic Model Structure

The simplest ActiveRecord model is just a class that inherits from ApplicationRecord:

class User < ApplicationRecord
  # This is all you need for a basic model!
  # ActiveRecord automatically:
  # - Connects to the 'users' table
  # - Provides methods like find, create, update, destroy
  # - Handles database operations
end

What ActiveRecord Gives You Automatically

  • User.find(id) - Find a record by ID
  • User.all - Get all records
  • User.create(attributes) - Create and save a record
  • User.new(attributes) - Create without saving
  • user.save - Save a record to database
  • user.destroy - Delete a record
Naming Conventions

ActiveRecord follows strict naming conventions that make your code more predictable and require less configuration:

DatabaseModel ClassExample
Table name (plural, snake_case)Model name (singular, PascalCase)usersUser
Table name (plural, snake_case)Model name (singular, PascalCase)blog_postsBlogPost
Primary key columnAlways named 'id'id (auto-incrementing)
Foreign key columnsEnd with '_id'user_id, post_id
TimestampsAutomatic columnscreated_at, updated_at
Important: If you don't follow these conventions, you'll need to configure everything manually, which defeats the purpose of using Rails!
Model Attributes & Methods

ActiveRecord automatically creates attributes and methods based on your database table structure:

# Database table: users
# | id | name | email | age | created_at | updated_at |
# |----|------|-------|-----|------------|------------|
# | 1  | John | [email protected] | 25 | 2024-01-01 | 2024-01-01 |

class User < ApplicationRecord
  # ActiveRecord automatically provides:
  # - @id, @name, @email, @age, @created_at, @updated_at
  # - name=, email=, age= methods for setting values
  # - name, email, age methods for getting values
end

# Usage:
user = User.find(1)
puts user.name                    # "John"
puts user.email                   # "[email protected]"
puts user.age                     # 25

user.name = "Johnny"              # Set a new value
user.save                         # Save to database

# Check if record is new or persisted
puts user.new_record?             # false (already saved)
puts user.persisted?              # true (exists in database)

Common Model Methods

Query Methods

  • User.find(id) - Find by primary key
  • User.find_by(attribute: value) - Find first match
  • User.where(conditions) - Find all matches
  • User.first - Get first record
  • User.last - Get last record
  • User.count - Count all records

Persistence Methods

  • user.save - Save to database
  • user.save! - Save or raise error
  • user.update(attributes) - Update and save
  • user.update!(attributes) - Update or raise error
  • user.destroy - Delete record
  • user.delete - Delete without callbacks
Advanced Model Features

Custom Methods

You can add your own methods to models for business logic:

class User < ApplicationRecord
  # Custom instance method
  def full_name
    "#{first_name} #{last_name}"
  end

  # Custom class method
  def self.adults
    where("age >= ?", 18)
  end

  # Method with parameters
  def age_in_years
    ((Time.current - created_at) / 1.year).floor
  end

  # Boolean method (convention: ends with ?)
  def adult?
    age >= 18
  end

  # Method that modifies the object
  def activate!
    update!(active: true)
  end
end

# Usage:
user = User.find(1)
puts user.full_name              # "John Doe"
puts user.adult?                 # true/false

adults = User.adults             # Get all adult users
user.activate!                   # Activate the user

Model Callbacks

Callbacks let you run code at specific points in a model's lifecycle:

class User < ApplicationRecord
  # Before callbacks
  before_create :set_default_role
  before_save :normalize_email
  
  # After callbacks
  after_create :send_welcome_email
  after_update :log_changes
  
  # Around callbacks
  around_save :log_save_time
  
  private
  
  def set_default_role
    self.role ||= 'user'
  end
  
  def normalize_email
    self.email = email.downcase.strip
  end
  
  def send_welcome_email
    UserMailer.welcome(self).deliver_now
  end
  
  def log_changes
    Rails.logger.info "User #{id} was updated"
  end
  
  def log_save_time
    start_time = Time.current
    yield  # This saves the record
    Rails.logger.info "Save took #{Time.current - start_time} seconds"
  end
end

Model Validations

Validations ensure data integrity before saving to the database:

class User < ApplicationRecord
  # Presence validations
  validates :name, presence: true
  validates :email, presence: true
  
  # Uniqueness validations
  validates :email, uniqueness: true
  validates :username, uniqueness: { case_sensitive: false }
  
  # Format validations
  validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :phone, format: { with: /\A\+?\d{10,14}\z/ }
  
  # Length validations
  validates :name, length: { minimum: 2, maximum: 50 }
  validates :bio, length: { maximum: 500 }
  
  # Numericality validations
  validates :age, numericality: { greater_than: 0, less_than: 120 }
  validates :score, numericality: { only_integer: true }
  
  # Custom validations
  validate :email_domain_allowed
  
  private
  
  def email_domain_allowed
    unless email.end_with?('@example.com', '@gmail.com')
      errors.add(:email, 'must be from allowed domain')
    end
  end
end
Model Best Practices

Keep Models Thin

Models should focus on data and business logic, not presentation or HTTP concerns:

✅ Good - Model handles data logic

class User < ApplicationRecord
  validates :email, presence: true, uniqueness: true
  
  def full_name
    "#{first_name} #{last_name}"
  end
  
  def can_edit?(post)
    admin? || post.user_id == id
  end
end

❌ Bad - Model handles presentation

class User < ApplicationRecord
  def to_html
    "
#{name}
" end def send_http_request # HTTP logic doesn't belong in models end end

Use Scopes for Common Queries

Scopes make your code more readable and reusable:

class User < ApplicationRecord
  # Good: Use scopes for common queries
  scope :active, -> { where(active: true) }
  scope :recent, -> { where('created_at > ?', 1.week.ago) }
  scope :by_role, ->(role) { where(role: role) }
  
  # Usage:
  User.active.recent.by_role('admin')
end

Handle Errors Gracefully

Use the bang methods (!) when you want to handle errors explicitly:

# Safe approach - returns false on validation failure
user = User.new(name: '')
if user.save
  # Success
else
  # Handle validation errors
  puts user.errors.full_messages
end

# Explicit approach - raises exception on failure
begin
  user = User.create!(name: '')
rescue ActiveRecord::RecordInvalid => e
  puts e.message
end
Pro Tip: Use rails console to experiment with your models interactively. It's a great way to test model methods and understand how ActiveRecord works!

Migrations

Migrations are Ruby classes that track changes to your database schema over time. They allow you to version control your database structure and share changes with your team.

What are Migrations? Think of migrations as a way to evolve your database schema step by step, with each migration representing a single change to your database structure.
Basic Migration Structure

Every migration is a Ruby class that inherits from ActiveRecord::Migration and contains a change method:

class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :name
      t.string :email
      t.timestamps
    end
  end
end

Migration Components

  • Class name: CreateUsers - Describes what the migration does
  • Inheritance: ActiveRecord::Migration[7.0] - Rails version
  • Change method: Contains the actual database operations
  • Table operations: create_table, add_column, etc.

Generating Migrations

Use Rails generators to create migrations:

# Generate a migration to create a table
rails generate migration CreateUsers name:string email:string

# Generate a migration to add a column
rails generate migration AddAgeToUsers age:integer

# Generate a migration to remove a column
rails generate migration RemoveEmailFromUsers email:string

# Generate a migration to add an index
rails generate migration AddIndexToUsersEmail
Creating Tables

The most common migration creates a new table:

class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      # String columns
      t.string :name, null: false
      t.string :email, null: false, limit: 255
      
      # Text columns (for longer content)
      t.text :bio
      
      # Integer columns
      t.integer :age
      t.integer :score, default: 0
      
      # Decimal columns (for money, precise numbers)
      t.decimal :price, precision: 10, scale: 2
      
      # Boolean columns
      t.boolean :active, default: true
      
      # Date/Time columns
      t.date :birth_date
      t.datetime :last_login_at
      
      # Timestamps (created_at, updated_at)
      t.timestamps
    end
    
    # Add indexes for better performance
    add_index :users, :email, unique: true
    add_index :users, :name
  end
end

Common Column Types

TypeDescriptionExample
stringShort text (VARCHAR)t.string :name
textLong text (TEXT)t.text :bio
integerWhole numberst.integer :age
decimalPrecise decimal numberst.decimal :price, precision: 10, scale: 2
booleanTrue/false valuest.boolean :active
dateDate onlyt.date :birth_date
datetimeDate and timet.datetime :last_login_at
timestampscreated_at and updated_att.timestamps
Modifying Tables

Adding Columns

class AddAgeToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :age, :integer
    add_column :users, :bio, :text
    add_column :users, :active, :boolean, default: true
  end
end

Removing Columns

class RemoveEmailFromUsers < ActiveRecord::Migration[7.0]
  def change
    remove_column :users, :email
    remove_column :users, :old_field
  end
end

Renaming Columns

class RenameNameToFullName < ActiveRecord::Migration[7.0]
  def change
    rename_column :users, :name, :full_name
  end
end

Changing Column Types

class ChangeAgeToInteger < ActiveRecord::Migration[7.0]
  def change
    change_column :users, :age, :integer
    change_column :users, :price, :decimal, precision: 10, scale: 2
  end
end

Adding Indexes

class AddIndexesToUsers < ActiveRecord::Migration[7.0]
  def change
    # Regular index
    add_index :users, :name
    
    # Unique index
    add_index :users, :email, unique: true
    
    # Composite index (multiple columns)
    add_index :users, [:first_name, :last_name]
    
    # Index with custom name
    add_index :users, :created_at, name: 'index_users_on_created_at'
  end
end

Removing Indexes

class RemoveIndexesFromUsers < ActiveRecord::Migration[7.0]
  def change
    remove_index :users, :name
    remove_index :users, :email
  end
end
Advanced Migration Features

Using Up and Down Methods

For complex migrations, you can define separate up and down methods:

class ComplexUserMigration < ActiveRecord::Migration[7.0]
  def up
    # This runs when migrating up (forward)
    create_table :users do |t|
      t.string :name
      t.string :email
      t.timestamps
    end
    
    # Add some initial data
    execute "INSERT INTO users (name, email) VALUES ('Admin', '[email protected]')"
  end

  def down
    # This runs when rolling back
    drop_table :users
  end
end

Conditional Migrations

class AddColumnIfNotExists < ActiveRecord::Migration[7.0]
  def change
    # Only add column if it doesn't exist
    unless column_exists?(:users, :age)
      add_column :users, :age, :integer
    end
  end
end

Data Migrations

class UpdateUserData < ActiveRecord::Migration[7.0]
  def up
    # Update existing data
    User.where(role: nil).update_all(role: 'user')
    
    # Execute raw SQL if needed
    execute "UPDATE users SET status = 'active' WHERE status IS NULL"
  end

  def down
    # Revert the changes
    User.where(role: 'user').update_all(role: nil)
    execute "UPDATE users SET status = NULL WHERE status = 'active'"
  end
end

Foreign Key Constraints

class AddForeignKeys < ActiveRecord::Migration[7.0]
  def change
    # Add foreign key with constraint
    add_reference :posts, :user, foreign_key: true
    
    # Or manually add foreign key
    add_foreign_key :posts, :users, column: :author_id
    
    # Remove foreign key
    remove_foreign_key :posts, :users
  end
end
Running Migrations

Migration Commands

# Run all pending migrations
rails db:migrate

# Run migrations up to a specific version
rails db:migrate VERSION=20240101000000

# Rollback the last migration
rails db:rollback

# Rollback multiple migrations
rails db:rollback STEP=3

# Reset database (drop, create, migrate)
rails db:reset

# Show migration status
rails db:migrate:status

# Show SQL that would be executed
rails db:migrate:status

Migration Status

Check which migrations have been run:

$ rails db:migrate:status

Status   Migration ID    Migration Name
--------------------------------------------------
   up     20240101000001  Create users
   up     20240101000002  Add email to users
 down     20240101000003  Add age to users
 down     20240101000004  Create posts

Migration Best Practices

✅ Good Practices

  • Keep migrations small and focused
  • Use descriptive migration names
  • Test migrations on a copy of production data
  • Never modify existing migrations in production
  • Use change method when possible

❌ Avoid These

  • Large migrations that change many things
  • Modifying migrations that have been run in production
  • Adding business logic to migrations
  • Forgetting to add indexes for foreign keys
  • Not testing rollbacks
Important: Never modify migrations that have been run in production. Instead, create a new migration to make the changes you need.

Associations

Associations are ActiveRecord's way of defining relationships between models. They allow you to work with related data as if they were regular Ruby objects, making your code more intuitive and readable.

What are Associations? Think of associations as the way ActiveRecord models "know" about each other. They create convenient methods for accessing related data without writing complex SQL queries.
Basic Association Types

ActiveRecord provides four main types of associations, each representing a different relationship pattern:

Association TypeRelationshipForeign KeyExample
belongs_toOne-to-One (child)In this modelbelongs_to :author
has_oneOne-to-One (parent)In other modelhas_one :profile
has_manyOne-to-ManyIn other modelhas_many :posts
has_many :throughMany-to-ManyIn join tablehas_many :categories, through: :posts

Understanding Foreign Keys

Foreign keys are the database columns that create the relationships between tables:

# Database tables:
# users table: id, name, email
# posts table: id, title, content, user_id  ← This is the foreign key
# profiles table: id, bio, avatar, user_id  ← This is the foreign key

# In your models:
class User < ApplicationRecord
  has_many :posts      # User has many posts
  has_one :profile     # User has one profile
end

class Post < ApplicationRecord
  belongs_to :user     # Post belongs to a user
end

class Profile < ApplicationRecord
  belongs_to :user     # Profile belongs to a user
end
One-to-Many Relationships

The most common relationship is one-to-many, where one model has many instances of another model.

Basic One-to-Many

class User < ApplicationRecord
  has_many :posts
end

class Post < ApplicationRecord
  belongs_to :user
end

# Usage:
user = User.find(1)
user.posts                    # Get all posts by this user
user.posts.create(title: "Hello")  # Create a post for this user

post = Post.find(1)
post.user                     # Get the user who wrote this post
post.user.name                # Get the user's name

Advanced One-to-Many Options

class User < ApplicationRecord
  has_many :posts, dependent: :destroy
  has_many :comments, -> { where(approved: true) }
  has_many :drafts, class_name: 'Post', foreign_key: 'author_id'
  has_many :recent_posts, -> { where('created_at > ?', 1.week.ago) }, class_name: 'Post'
end

class Post < ApplicationRecord
  belongs_to :user, optional: true
  belongs_to :author, class_name: 'User', foreign_key: 'author_id'
end

Common Options

OptionDescriptionExample
dependentWhat happens when parent is deleteddependent: :destroy
class_nameSpecify the model class nameclass_name: 'Post'
foreign_keySpecify the foreign key columnforeign_key: 'author_id'
optionalAllow null foreign keysoptional: true
One-to-One Relationships

One-to-one relationships are used when one model has exactly one instance of another model.

Basic One-to-One

class User < ApplicationRecord
  has_one :profile
end

class Profile < ApplicationRecord
  belongs_to :user
end

# Usage:
user = User.find(1)
user.profile                   # Get the user's profile
user.build_profile(bio: "Hello")  # Create a profile for this user

profile = Profile.find(1)
profile.user                   # Get the user who owns this profile

One-to-One with Options

class User < ApplicationRecord
  has_one :profile, dependent: :destroy
  has_one :avatar, class_name: 'Image', foreign_key: 'user_id'
end

class Profile < ApplicationRecord
  belongs_to :user, optional: true
end

# Common use cases:
# - User has one Profile
# - User has one Avatar
# - Post has one FeaturedImage
# - Order has one ShippingAddress
Many-to-Many Relationships

Many-to-many relationships use a join table to connect two models that can have multiple instances of each other.

Using has_many :through

# Database tables:
# users table: id, name
# posts table: id, title, user_id
# categories table: id, name
# post_categories table: post_id, category_id

class User < ApplicationRecord
  has_many :posts
  has_many :categories, through: :posts
end

class Post < ApplicationRecord
  belongs_to :user
  has_many :post_categories
  has_many :categories, through: :post_categories
end

class Category < ApplicationRecord
  has_many :post_categories
  has_many :posts, through: :post_categories
end

class PostCategory < ApplicationRecord
  belongs_to :post
  belongs_to :category
end

# Usage:
user = User.find(1)
user.categories                # Get all categories from user's posts

post = Post.find(1)
post.categories                # Get all categories for this post
post.categories << Category.find(1)  # Add a category to this post

Using has_and_belongs_to_many (HABTM)

# Simpler many-to-many without a join model
# Database tables:
# users table: id, name
# skills table: id, name
# skills_users table: skill_id, user_id

class User < ApplicationRecord
  has_and_belongs_to_many :skills
end

class Skill < ApplicationRecord
  has_and_belongs_to_many :users
end

# Usage:
user = User.find(1)
user.skills                    # Get all skills for this user
user.skills << Skill.find(1)   # Add a skill to this user

skill = Skill.find(1)
skill.users                    # Get all users with this skill

When to Use Each Approach

Use has_many :through when:

  • You need additional data on the join table
  • You want to add validations to the join model
  • You need to query the join table directly
  • You want more control over the relationship

Use has_and_belongs_to_many when:

  • The join table only contains foreign keys
  • You don't need additional data on the join
  • You want a simpler setup
  • You don't need to query the join table
Association Methods & Options

Methods Created by Associations

class User < ApplicationRecord
  has_many :posts
  has_one :profile
end

class Post < ApplicationRecord
  belongs_to :user
end

# has_many creates these methods:
user.posts                    # Get all posts
user.posts.create(attributes) # Create a new post
user.posts.build(attributes)  # Build a new post (not saved)
user.posts << post           # Add an existing post
user.posts.delete(post)      # Remove a post
user.posts.destroy(post)     # Remove and destroy a post
user.posts.clear             # Remove all posts
user.posts.empty?            # Check if no posts
user.posts.size              # Count posts
user.posts.find(id)          # Find a specific post
user.posts.where(conditions) # Find posts with conditions

# has_one creates these methods:
user.profile                  # Get the profile
user.build_profile(attributes) # Build a new profile
user.create_profile(attributes) # Create a new profile
user.profile = profile       # Assign a profile

# belongs_to creates these methods:
post.user                     # Get the user
post.build_user(attributes)   # Build a new user
post.create_user(attributes)  # Create a new user
post.user = user             # Assign a user

Common Association Options

OptionDescriptionExample
dependentWhat happens when parent is deleteddependent: :destroy
class_nameSpecify the model class nameclass_name: 'Post'
foreign_keySpecify the foreign key columnforeign_key: 'author_id'
primary_keySpecify the primary key columnprimary_key: 'uuid'
optionalAllow null foreign keysoptional: true
counter_cacheCache the count of associated recordscounter_cache: :posts_count

Dependent Options

class User < ApplicationRecord
  # Destroy associated records when user is destroyed
  has_many :posts, dependent: :destroy
  
  # Set foreign key to NULL when user is destroyed
  has_many :comments, dependent: :nullify
  
  # Delete associated records without callbacks
  has_many :logs, dependent: :delete_all
  
  # Raise error if user has associated records
  has_many :orders, dependent: :restrict_with_error
  
  # Don't destroy if user has associated records
  has_many :accounts, dependent: :restrict_with_exception
end
Polymorphic Associations

Polymorphic associations allow a model to belong to more than one type of model on a single association.

Basic Polymorphic Association

# Database tables:
# users table: id, name
# posts table: id, title
# comments table: id, content, commentable_type, commentable_id

class User < ApplicationRecord
  has_many :posts
  has_many :comments, as: :commentable
end

class Post < ApplicationRecord
  belongs_to :user
  has_many :comments, as: :commentable
end

class Comment < ApplicationRecord
  belongs_to :commentable, polymorphic: true
end

# Usage:
user = User.find(1)
user.comments                    # Get all comments on this user

post = Post.find(1)
post.comments                    # Get all comments on this post

comment = Comment.find(1)
comment.commentable              # Get the user or post this comment belongs to

Common Polymorphic Use Cases

# Likes system
class Like < ApplicationRecord
  belongs_to :likeable, polymorphic: true
  belongs_to :user
end

class User < ApplicationRecord
  has_many :likes
  has_many :posts
end

class Post < ApplicationRecord
  has_many :likes, as: :likeable
end

# File attachments
class Attachment < ApplicationRecord
  belongs_to :attachable, polymorphic: true
end

class User < ApplicationRecord
  has_many :attachments, as: :attachable
end

class Post < ApplicationRecord
  has_many :attachments, as: :attachable
end

# Activity feeds
class Activity < ApplicationRecord
  belongs_to :trackable, polymorphic: true
  belongs_to :owner, polymorphic: true
end
Association Best Practices

Naming Conventions

# ✅ Good - Clear, descriptive names
class User < ApplicationRecord
  has_many :posts
  has_one :profile
  has_many :comments, as: :commentable
end

# ❌ Bad - Unclear names
class User < ApplicationRecord
  has_many :things
  has_one :stuff
  has_many :items, as: :itemable
end

Performance Considerations

# ✅ Good - Use includes to avoid N+1 queries
users = User.includes(:posts).all
users.each do |user|
  puts user.posts.count  # No additional queries
end

# ❌ Bad - N+1 query problem
users = User.all
users.each do |user|
  puts user.posts.count  # One query per user!
end

# ✅ Good - Use counter_cache for counts
class User < ApplicationRecord
  has_many :posts, counter_cache: :posts_count
end

class Post < ApplicationRecord
  belongs_to :user, counter_cache: :posts_count
end

Validation with Associations

class User < ApplicationRecord
  has_many :posts, dependent: :destroy
  
  # Validate that user has at least one post
  validate :must_have_posts
  
  private
  
  def must_have_posts
    if posts.empty?
      errors.add(:base, "User must have at least one post")
    end
  end
end

class Post < ApplicationRecord
  belongs_to :user
  
  # Validate that post belongs to an active user
  validate :user_must_be_active
  
  private
  
  def user_must_be_active
    unless user&.active?
      errors.add(:user, "must be active")
    end
  end
end

Common Patterns

✅ Good Patterns

  • Use descriptive association names
  • Add appropriate dependent options
  • Use includes to avoid N+1 queries
  • Add counter_cache for frequently counted associations
  • Use polymorphic associations for flexible relationships

❌ Avoid These

  • Circular dependencies between models
  • Too many associations in a single model
  • Forgetting to add indexes on foreign keys
  • Using associations for temporary data
  • Not considering performance implications
Pro Tip: Use rails console to experiment with associations. Try creating related objects and see what methods are available!

Validations

Validations are ActiveRecord's way of ensuring data integrity before records are saved to the database. They help prevent invalid data from entering your system and provide clear error messages to users.

What are Validations? Think of validations as a security checkpoint for your data. They run before a record is saved and can prevent the save if the data doesn't meet your requirements.
Basic Validation Structure

Validations are declared in your model using the validates method:

class User < ApplicationRecord
  validates :name, presence: true
  validates :email, presence: true, uniqueness: true
  validates :age, numericality: { greater_than: 0 }
end

# Usage:
user = User.new(name: "", email: "[email protected]")
if user.save
  puts "User saved successfully!"
else
  puts user.errors.full_messages
  # Output: ["Name can't be blank"]
end

When Validations Run

  • save - Validations run, returns true/false
  • save! - Validations run, raises exception on failure
  • create - Validations run, returns object
  • create! - Validations run, raises exception on failure
  • update - Validations run, returns true/false
  • update! - Validations run, raises exception on failure

Validation Callbacks

class User < ApplicationRecord
  # Validations run in this order:
  # 1. before_validation callbacks
  # 2. validations
  # 3. after_validation callbacks
  
  before_validation :normalize_email
  validates :email, presence: true, uniqueness: true
  
  private
  
  def normalize_email
    self.email = email.downcase.strip if email.present?
  end
end
Presence Validations

Presence validations ensure that attributes are not empty or nil.

Basic Presence

class User < ApplicationRecord
  validates :name, presence: true
  validates :email, presence: true
  validates :bio, presence: true, allow_blank: false
end

# What gets validated:
user = User.new
user.valid?  # false
user.errors[:name]  # ["can't be blank"]

user = User.new(name: "   ")  # Only whitespace
user.valid?  # false (whitespace is considered blank)

user = User.new(name: "John")
user.valid?  # true

Presence with Associations

class Post < ApplicationRecord
  belongs_to :user
  validates :user, presence: true  # Validates the association
  validates :user_id, presence: true  # Validates the foreign key
end

# Usage:
post = Post.new(title: "Hello")
post.valid?  # false
post.errors[:user]  # ["must exist"]

post.user = User.find(1)
post.valid?  # true

Conditional Presence

class User < ApplicationRecord
  # Only validate presence if user is active
  validates :phone, presence: true, if: :active?
  
  # Validate presence unless user is admin
  validates :bio, presence: true, unless: :admin?
  
  # Custom condition
  validates :emergency_contact, presence: true, if: -> { age > 18 }
  
  private
  
  def active?
    status == 'active'
  end
  
  def admin?
    role == 'admin'
  end
end
Uniqueness Validations

Uniqueness validations ensure that no two records have the same value for a specific attribute.

Basic Uniqueness

class User < ApplicationRecord
  validates :email, uniqueness: true
  validates :username, uniqueness: true, case_sensitive: false
end

# Usage:
user1 = User.create(email: "[email protected]")
user2 = User.new(email: "[email protected]")
user2.valid?  # false
user2.errors[:email]  # ["has already been taken"]

Uniqueness with Scope

class Post < ApplicationRecord
  belongs_to :user
  
  # Title must be unique per user
  validates :title, uniqueness: { scope: :user_id }
  
  # Email must be unique per organization
  validates :email, uniqueness: { scope: [:organization_id, :deleted_at] }
end

# Usage:
user1 = User.find(1)
user2 = User.find(2)

post1 = user1.posts.create(title: "My Post")
post2 = user2.posts.new(title: "My Post")  # This is valid (different user)
post3 = user1.posts.new(title: "My Post")  # This is invalid (same user)

Uniqueness Options

OptionDescriptionExample
case_sensitiveCase-sensitive uniqueness checkcase_sensitive: false
scopeUniqueness within a scopescope: :user_id
allow_blankAllow blank values to be uniqueallow_blank: true
allow_nilAllow nil values to be uniqueallow_nil: true
Length Validations

Length validations ensure that string attributes meet specific length requirements.

Basic Length

class User < ApplicationRecord
  validates :name, length: { minimum: 2, maximum: 50 }
  validates :bio, length: { maximum: 500 }
  validates :username, length: { in: 3..20 }
  validates :password, length: { is: 8 }
end

# Usage:
user = User.new(name: "A")  # Too short
user.valid?  # false
user.errors[:name]  # ["is too short (minimum is 2 characters)"]

user = User.new(name: "John Doe")  # Valid
user.valid?  # true

Length with Custom Messages

class User < ApplicationRecord
  validates :name, length: { 
    minimum: 2, 
    maximum: 50,
    too_short: "must have at least %{count} characters",
    too_long: "must have at most %{count} characters"
  }
  
  validates :bio, length: { 
    maximum: 500,
    message: "is too long (maximum %{count} characters)"
  }
end

Length Options

OptionDescriptionExample
minimumMinimum length requiredminimum: 2
maximumMaximum length allowedmaximum: 50
inLength within a rangein: 3..20
isExact length requiredis: 8
allow_blankSkip validation if blankallow_blank: true
Format Validations

Format validations use regular expressions to ensure attributes match specific patterns.

Basic Format

class User < ApplicationRecord
  validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :phone, format: { with: /\A\+?\d{10,14}\z/ }
  validates :website, format: { with: URI::regexp(%w[http https]) }
end

# Usage:
user = User.new(email: "invalid-email")
user.valid?  # false
user.errors[:email]  # ["is invalid"]

user = User.new(email: "[email protected]")
user.valid?  # true

Common Format Patterns

class User < ApplicationRecord
  # Email validation
  validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
  
  # Phone number (US format)
  validates :phone, format: { with: /\A\(\d{3}\) \d{3}-\d{4}\z/ }
  
  # URL validation
  validates :website, format: { with: URI::regexp(%w[http https]) }
  
  # Username (alphanumeric + underscore)
  validates :username, format: { with: /\A[a-zA-Z0-9_]+\z/ }
  
  # Credit card number
  validates :credit_card, format: { with: /\A\d{4}-\d{4}-\d{4}-\d{4}\z/ }
  
  # Date format (YYYY-MM-DD)
  validates :birth_date, format: { with: /\A\d{4}-\d{2}-\d{2}\z/ }
end

Format with Custom Messages

class User < ApplicationRecord
  validates :email, format: { 
    with: URI::MailTo::EMAIL_REGEXP,
    message: "must be a valid email address"
  }
  
  validates :phone, format: { 
    with: /\A\+?\d{10,14}\z/,
    message: "must be a valid phone number"
  }
end
Numericality Validations

Numericality validations ensure that attributes contain numeric values and meet specific numeric requirements.

Basic Numericality

class User < ApplicationRecord
  validates :age, numericality: true
  validates :score, numericality: { only_integer: true }
  validates :price, numericality: { greater_than: 0 }
  validates :rating, numericality: { in: 1..5 }
end

# Usage:
user = User.new(age: "not a number")
user.valid?  # false
user.errors[:age]  # ["is not a number"]

user = User.new(age: 25, score: 100.5)
user.valid?  # false (score must be integer)
user.errors[:score]  # ["must be an integer"]

Numericality Options

OptionDescriptionExample
only_integerMust be an integeronly_integer: true
greater_thanMust be greater than valuegreater_than: 0
greater_than_or_equal_toMust be greater than or equal to valuegreater_than_or_equal_to: 0
less_thanMust be less than valueless_than: 100
less_than_or_equal_toMust be less than or equal to valueless_than_or_equal_to: 100
equal_toMust be equal to valueequal_to: 42
other_thanMust be different from valueother_than: 0
inMust be in rangein: 1..10

Advanced Numericality

class Product < ApplicationRecord
  validates :price, numericality: { 
    greater_than: 0,
    less_than: 10000,
    message: "must be between $0 and $10,000"
  }
  
  validates :quantity, numericality: { 
    only_integer: true,
    greater_than_or_equal_to: 0,
    message: "must be a non-negative integer"
  }
  
  validates :rating, numericality: { 
    in: 1..5,
    message: "must be between 1 and 5"
  }
  
  validates :discount, numericality: { 
    greater_than_or_equal_to: 0,
    less_than_or_equal_to: 100,
    message: "must be between 0% and 100%"
  }
end
Custom Validations

Custom validations allow you to create your own validation logic when built-in validations aren't sufficient.

Basic Custom Validation

class User < ApplicationRecord
  validates :email, presence: true
  validate :email_domain_allowed
  validate :age_must_be_reasonable
  
  private
  
  def email_domain_allowed
    return if email.blank?
    
    allowed_domains = ['gmail.com', 'yahoo.com', 'example.com']
    domain = email.split('@').last
    
    unless allowed_domains.include?(domain)
      errors.add(:email, "must be from an allowed domain")
    end
  end
  
  def age_must_be_reasonable
    return if age.blank?
    
    if age < 0 || age > 150
      errors.add(:age, "must be between 0 and 150")
    end
  end
end

Custom Validation with Parameters

class User < ApplicationRecord
  validate :password_strength
  
  private
  
  def password_strength
    return if password.blank?
    
    # Check minimum length
    if password.length < 8
      errors.add(:password, "must be at least 8 characters")
    end
    
    # Check for uppercase letter
    unless password.match?(/[A-Z]/)
      errors.add(:password, "must contain at least one uppercase letter")
    end
    
    # Check for lowercase letter
    unless password.match?(/[a-z]/)
      errors.add(:password, "must contain at least one lowercase letter")
    end
    
    # Check for number
    unless password.match?(/\d/)
      errors.add(:password, "must contain at least one number")
    end
    
    # Check for special character
    unless password.match?(/[!@#$%^&*]/)
      errors.add(:password, "must contain at least one special character")
    end
  end
end

Conditional Custom Validations

class User < ApplicationRecord
  validate :admin_requires_approval, if: :admin?
  validate :user_must_be_active, unless: :new_record?
  
  private
  
  def admin_requires_approval
    unless approved?
      errors.add(:base, "Admin accounts must be approved")
    end
  end
  
  def user_must_be_active
    unless active?
      errors.add(:base, "User account must be active")
    end
  end
end

Cross-Attribute Validations

class Event < ApplicationRecord
  validate :end_date_after_start_date
  validate :start_date_not_in_past
  
  private
  
  def end_date_after_start_date
    return if start_date.blank? || end_date.blank?
    
    if end_date <= start_date
      errors.add(:end_date, "must be after start date")
    end
  end
  
  def start_date_not_in_past
    return if start_date.blank?
    
    if start_date < Date.current
      errors.add(:start_date, "cannot be in the past")
    end
  end
end
Validation Best Practices

When to Use Validations

✅ Use Validations For:

  • Data integrity and consistency
  • Business rules and constraints
  • User input validation
  • Preventing invalid data from being saved
  • Providing clear error messages to users

❌ Don't Use Validations For:

  • Complex business logic (use service objects)
  • External API calls (use callbacks or services)
  • Performance optimizations (use database constraints)
  • Temporary data processing

Performance Considerations

class User < ApplicationRecord
  # ✅ Good - Use database constraints for performance
  validates :email, uniqueness: true
  
  # ✅ Good - Use scoped uniqueness for complex cases
  validates :email, uniqueness: { scope: :organization_id }
  
  # ❌ Bad - Avoid complex validations that hit the database
  validate :check_complex_business_logic
  
  private
  
  def check_complex_business_logic
    # This runs on every save - expensive!
    related_records = SomeModel.where(complex_conditions)
    # ... complex logic
  end
end

Error Message Best Practices

class User < ApplicationRecord
  # ✅ Good - Clear, user-friendly messages
  validates :email, presence: { message: "Email address is required" }
  validates :age, numericality: { 
    greater_than: 0, 
    message: "Age must be greater than 0" 
  }
  
  # ❌ Bad - Technical, unclear messages
  validates :email, presence: { message: "Email cannot be nil" }
  validates :age, numericality: { 
    greater_than: 0, 
    message: "Must be > 0" 
  }
end

Testing Validations

# In your test file
require 'rails_helper'

RSpec.describe User, type: :model do
  describe 'validations' do
    it 'is valid with valid attributes' do
      user = User.new(name: 'John', email: '[email protected]')
      expect(user).to be_valid
    end
    
    it 'is invalid without a name' do
      user = User.new(email: '[email protected]')
      expect(user).not_to be_valid
      expect(user.errors[:name]).to include("can't be blank")
    end
    
    it 'is invalid with duplicate email' do
      User.create!(name: 'John', email: '[email protected]')
      user = User.new(name: 'Jane', email: '[email protected]')
      expect(user).not_to be_valid
      expect(user.errors[:email]).to include("has already been taken")
    end
  end
end
Pro Tip: Use valid? and invalid? methods to check validation status, and errors.full_messages to get all error messages for display to users.

Scopes

Scopes are custom query methods that encapsulate commonly used queries and can be chained together.

What are Scopes? Scopes are reusable query fragments that make your code more readable and maintainable. They're like custom methods for your queries.
Default Scopes

Default scopes are automatically applied to all queries for a model.

Basic Default Scope

class User < ApplicationRecord
  # Default scope - always order by created_at
  default_scope { order(created_at: :desc) }
  
  # Or with lambda syntax
  default_scope -> { order(created_at: :desc) }
end

# Usage - automatically ordered
User.all  # Returns users ordered by created_at DESC
User.where(active: true)  # Also ordered by created_at DESC

Default Scope with Conditions

class Post < ApplicationRecord
  # Default scope - only published posts
  default_scope { where(published: true) }
  
  # Or with lambda syntax
  default_scope -> { where(published: true) }
end

# Usage - automatically filters published posts
Post.all  # Returns only published posts
Post.where(category: 'tech')  # Only published tech posts

Bypassing Default Scopes

class User < ApplicationRecord
  default_scope { order(created_at: :desc) }
end

# Bypass default scope
User.unscoped.all  # Returns all users without ordering
User.unscoped.where(active: true)  # Without default scope

# Or use unscoped for specific queries
User.unscoped.find(1)  # Find without default scope
Warning: Be careful with default scopes as they're applied to ALL queries. Consider using regular scopes instead for better control.
Custom Scopes

Custom scopes are reusable query methods that you define yourself.

Basic Custom Scopes

class User < ApplicationRecord
  # Simple scope
  scope :active, -> { where(active: true) }
  
  # Scope with parameters
  scope :by_role, ->(role) { where(role: role) }
  
  # Scope with multiple parameters
  scope :recent, ->(days = 30) { where('created_at > ?', days.days.ago) }
  
  # Scope with complex conditions
  scope :verified_adults, -> { where(verified: true).where('age >= ?', 18) }
end

# Usage
User.active                    # All active users
User.by_role('admin')         # All admin users
User.recent(7)                # Users created in last 7 days
User.verified_adults          # Verified users 18 or older

Chaining Scopes

class User < ApplicationRecord
  scope :active, -> { where(active: true) }
  scope :by_role, ->(role) { where(role: role) }
  scope :recent, ->(days = 30) { where('created_at > ?', days.days.ago) }
  scope :verified, -> { where(verified: true) }
end

# Chain multiple scopes
User.active.by_role('admin').recent(7)
User.verified.by_role('moderator').recent(30)

# Complex chaining
User.active
    .by_role('admin')
    .recent(7)
    .order(:created_at)
    .limit(10)

Scopes with Associations

class User < ApplicationRecord
  has_many :posts
  
  # Scope for users with posts
  scope :with_posts, -> { joins(:posts).distinct }
  
  # Scope for users with recent posts
  scope :with_recent_posts, -> { 
    joins(:posts).where('posts.created_at > ?', 1.week.ago).distinct 
  }
  
  # Scope for users with many posts
  scope :prolific_writers, -> { 
    joins(:posts).group('users.id').having('COUNT(posts.id) > 5') 
  }
end

# Usage
User.with_posts                    # Users who have written posts
User.with_recent_posts             # Users with posts in last week
User.prolific_writers              # Users with more than 5 posts
Advanced Scopes

Advanced scopes for complex query patterns and business logic.

Conditional Scopes

class User < ApplicationRecord
  # Scope that changes based on conditions
  scope :searchable, ->(query = nil) {
    if query.present?
      where('name ILIKE ? OR email ILIKE ?', "%#{query}%", "%#{query}%")
    else
      all
    end
  }
  
  # Scope with multiple conditions
  scope :filtered, ->(filters = {}) {
    relation = all
    relation = relation.where(role: filters[:role]) if filters[:role]
    relation = relation.where(active: filters[:active]) if filters[:active]
    relation = relation.where('created_at > ?', filters[:since]) if filters[:since]
    relation
  }
end

# Usage
User.searchable('john')           # Users with 'john' in name or email
User.searchable                   # All users (no query)
User.filtered(role: 'admin', active: true)  # Active admins

Scopes with Calculations

class Order < ApplicationRecord
  belongs_to :user
  
  # Scope for high-value orders
  scope :high_value, -> { where('total_amount > ?', 1000) }
  
  # Scope for orders by value range
  scope :by_value_range, ->(min, max) { 
    where(total_amount: min..max) 
  }
  
  # Scope for orders with discounts
  scope :with_discount, -> { where('discount_amount > 0') }
  
  # Scope for orders by status
  scope :by_status, ->(status) { where(status: status) }
end

# Usage
Order.high_value                  # Orders over $1000
Order.by_value_range(500, 2000)  # Orders between $500-$2000
Order.with_discount              # Orders with discounts
Order.by_status('pending')       # Pending orders

Scopes with Time Ranges

class Post < ApplicationRecord
  # Scope for posts by time period
  scope :today, -> { where('created_at >= ?', Date.current.beginning_of_day) }
  scope :this_week, -> { where('created_at >= ?', Date.current.beginning_of_week) }
  scope :this_month, -> { where('created_at >= ?', Date.current.beginning_of_month) }
  scope :last_month, -> { 
    where(created_at: 1.month.ago.beginning_of_month..1.month.ago.end_of_month) 
  }
  
  # Scope for recent posts
  scope :recent, ->(days = 7) { where('created_at > ?', days.days.ago) }
  
  # Scope for posts by date range
  scope :between_dates, ->(start_date, end_date) {
    where(created_at: start_date.beginning_of_day..end_date.end_of_day)
  }
end

# Usage
Post.today                       # Posts created today
Post.this_week                   # Posts created this week
Post.this_month                  # Posts created this month
Post.recent(30)                 # Posts from last 30 days
Post.between_dates(Date.yesterday, Date.today)  # Posts from yesterday to today
Best Practices:
  • Use scopes to make your code more readable and reusable
  • Chain scopes together for complex queries
  • Avoid default scopes unless absolutely necessary
  • Keep scopes simple and focused on one responsibility
  • Use parameters in scopes for flexibility

Enums

Enums allow you to define a set of named values for a column, making your code more readable and maintainable.

What are Enums? Enums map integer values in the database to meaningful names in your Ruby code, providing both type safety and readability.
Basic Enums

Define enums to create readable constants and helper methods.

Simple Enum Definition

class User < ApplicationRecord
  # Define enum for status column
  enum status: { pending: 0, active: 1, inactive: 2, banned: 3 }
  
  # Or with string values
  enum role: { user: 'user', admin: 'admin', moderator: 'moderator' }
end

# Database column: status (integer)
# | id | name  | status |
# |----|-------|--------|
# | 1  | John  | 1      | (active)
# | 2  | Jane  | 0      | (pending)
# | 3  | Bob   | 3      | (banned)

Enum Helper Methods

class User < ApplicationRecord
  enum status: { pending: 0, active: 1, inactive: 2, banned: 3 }
  enum role: { user: 'user', admin: 'admin', moderator: 'moderator' }
end

# Instance methods generated automatically
user = User.find(1)

# Query methods
user.pending?     # => false (checks if status == 0)
user.active?      # => true  (checks if status == 1)
user.inactive?    # => false (checks if status == 2)
user.banned?      # => false (checks if status == 3)

# Assignment methods
user.pending!     # Sets status to 0
user.active!      # Sets status to 1
user.inactive!    # Sets status to 2
user.banned!      # Sets status to 3

# Class methods for querying
User.pending      # Returns all users with status = 0
User.active       # Returns all users with status = 1
User.inactive     # Returns all users with status = 2
User.banned       # Returns all users with status = 3

# Class methods for counting
User.pending.count    # Count of pending users
User.active.count     # Count of active users
Advanced Enums

Advanced enum features for complex scenarios.

Enums with Prefix and Suffix

class User < ApplicationRecord
  # Enum with prefix
  enum status: { pending: 0, active: 1, inactive: 2 }, _prefix: true
  
  # Enum with suffix
  enum role: { user: 'user', admin: 'admin' }, _suffix: true
  
  # Enum with custom prefix
  enum status: { pending: 0, active: 1, inactive: 2 }, _prefix: 'user_status'
end

# Usage with prefix
user.status_pending?     # => false
user.status_active?      # => true
user.status_inactive?    # => false

user.status_pending!     # Sets status to 0
user.status_active!      # Sets status to 1

User.status_pending      # All pending users
User.status_active       # All active users

# Usage with suffix
user.user?               # => true
user.admin?              # => false

user.user!               # Sets role to 'user'
user.admin!              # Sets role to 'admin'

User.user                # All users with role 'user'
User.admin               # All users with role 'admin'

Enums with Multiple Columns

class Order < ApplicationRecord
  # Multiple enums
  enum status: { pending: 0, processing: 1, shipped: 2, delivered: 3, cancelled: 4 }
  enum payment_status: { unpaid: 0, paid: 1, refunded: 2 }
  enum shipping_method: { standard: 0, express: 1, overnight: 2 }
end

# Usage
order = Order.find(1)

# Status methods
order.pending?           # => true
order.processing?        # => false
order.shipped?           # => false

order.processing!        # Sets status to 1

# Payment status methods
order.unpaid?            # => true
order.paid?              # => false

order.paid!              # Sets payment_status to 1

# Shipping method methods
order.standard?          # => true
order.express?           # => false

order.express!           # Sets shipping_method to 1

# Class methods
Order.pending            # All pending orders
Order.paid               # All paid orders
Order.standard           # All standard shipping orders

Enums with Custom Methods

class User < ApplicationRecord
  enum status: { pending: 0, active: 1, inactive: 2, banned: 3 }
  
  # Custom methods for enum values
  def can_login?
    active? && !banned?
  end
  
  def can_post?
    active? && !banned?
  end
  
  def can_comment?
    active? && !banned?
  end
  
  def status_display
    case status
    when 'pending'
      'Awaiting Approval'
    when 'active'
      'Active User'
    when 'inactive'
      'Inactive Account'
    when 'banned'
      'Account Banned'
    else
      'Unknown Status'
    end
  end
end

# Usage
user = User.find(1)

user.can_login?          # => true (if active and not banned)
user.can_post?           # => true (if active and not banned)
user.status_display      # => "Active User"
Best Practices:
  • Use enums for columns with a limited set of possible values
  • Choose meaningful names for enum values
  • Use prefixes/suffixes to avoid method name conflicts
  • Add custom methods to enums for business logic
  • Consider using string enums for better readability in database

Serialization

Serialization allows you to store complex data structures (arrays, hashes, objects) in database columns as JSON, YAML, or other formats.

What is Serialization? Serialization converts complex Ruby objects into a format that can be stored in the database, and deserialization converts them back to Ruby objects.
JSON Serialization

Store complex data as JSON in database columns.

Basic JSON Serialization

class User < ApplicationRecord
  # Serialize preferences as JSON
  serialize :preferences, JSON
  
  # Or use the newer syntax
  attribute :preferences, :json
end

# Database column: preferences (text/json)
# | id | name  | preferences                                    |
# |----|-------|------------------------------------------------|
# | 1  | John  | {"theme":"dark","notifications":true}          |
# | 2  | Jane  | {"theme":"light","notifications":false}       |

# Usage
user = User.find(1)
user.preferences = { theme: 'dark', notifications: true }
user.save

# Access the data
user.preferences['theme']           # => "dark"
user.preferences['notifications']   # => true

# Update the data
user.preferences['theme'] = 'light'
user.preferences['language'] = 'en'
user.save

JSON with Default Values

class User < ApplicationRecord
  attribute :preferences, :json, default: {}
  
  # Or with custom default
  attribute :settings, :json, default: -> { { theme: 'light', language: 'en' } }
end

# Usage
user = User.new
user.preferences                    # => {}
user.settings                       # => {"theme"=>"light", "language"=>"en"}

# Add to preferences
user.preferences[:notifications] = true
user.preferences[:timezone] = 'UTC'
user.save
YAML Serialization

Store complex data as YAML in database columns.

Basic YAML Serialization

class Product < ApplicationRecord
  # Serialize specifications as YAML
  serialize :specifications, YAML
  
  # Or use the newer syntax
  attribute :specifications, :yaml
end

# Database column: specifications (text)
# | id | name      | specifications                              |
# |----|-----------|--------------------------------------------|
# | 1  | iPhone    | ---\ncolor: black\nstorage: 128GB\n       |
# | 2  | MacBook   | ---\ncolor: silver\nstorage: 512GB\n      |

# Usage
product = Product.find(1)
product.specifications = {
  color: 'black',
  storage: '128GB',
  features: ['camera', 'fingerprint', 'face_id']
}
product.save

# Access the data
product.specifications['color']     # => "black"
product.specifications['storage']   # => "128GB"
product.specifications['features']  # => ["camera", "fingerprint", "face_id"]
Custom Serialization

Create custom serializers for complex data structures.

Custom Serializer Class

class AddressSerializer
  def self.dump(address)
    return nil if address.nil?
    
    {
      street: address.street,
      city: address.city,
      state: address.state,
      postal_code: address.postal_code,
      country: address.country
    }.to_json
  end
  
  def self.load(data)
    return nil if data.nil?
    
    address_data = JSON.parse(data)
    Address.new(
      street: address_data['street'],
      city: address_data['city'],
      state: address_data['state'],
      postal_code: address_data['postal_code'],
      country: address_data['country']
    )
  end
end

class User < ApplicationRecord
  serialize :shipping_address, AddressSerializer
end

# Address class
class Address
  attr_accessor :street, :city, :state, :postal_code, :country
  
  def initialize(attributes = {})
    @street = attributes[:street]
    @city = attributes[:city]
    @state = attributes[:state]
    @postal_code = attributes[:postal_code]
    @country = attributes[:country]
  end
  
  def full_address
    [street, city, state, postal_code, country].compact.join(', ')
  end
end

# Usage
user = User.find(1)
address = Address.new(
  street: '123 Main St',
  city: 'New York',
  state: 'NY',
  postal_code: '10001',
  country: 'USA'
)

user.shipping_address = address
user.save

# Access the serialized address
user.shipping_address.full_address  # => "123 Main St, New York, NY, 10001, USA"

Array Serialization

class Post < ApplicationRecord
  # Serialize tags as an array
  serialize :tags, Array
  
  # Serialize categories as an array with default
  serialize :categories, Array, default: []
end

# Usage
post = Post.find(1)
post.tags = ['ruby', 'rails', 'active-record']
post.categories = ['programming', 'tutorial']
post.save

# Access the arrays
post.tags                    # => ["ruby", "rails", "active-record"]
post.categories              # => ["programming", "tutorial"]

# Add to arrays
post.tags << 'database'
post.categories << 'web-development'
post.save
Best Practices:
  • Use JSON for simple data structures and API compatibility
  • Use YAML for complex nested structures
  • Create custom serializers for complex objects
  • Always provide default values for serialized columns
  • Consider query performance when using serialized columns

Callbacks

Callbacks are methods that get called at specific points in an ActiveRecord object's lifecycle. They allow you to run code before or after certain operations like creating, updating, or destroying records.

What are Callbacks? Think of callbacks as automatic triggers that fire at specific moments in your model's lifecycle. They're perfect for setting default values, cleaning up data, or performing side effects.
Callback Lifecycle

Understanding when callbacks run is crucial for using them effectively. Here's the complete lifecycle:

Create Callbacks

# When you call user.save or user.create:

# 1. before_validation
# 2. validations
# 3. after_validation
# 4. before_save
# 5. before_create
# 6. INSERT INTO database
# 7. after_create
# 8. after_save

class User < ApplicationRecord
  before_validation :normalize_email
  before_save :set_timestamps
  before_create :set_default_role
  after_create :send_welcome_email
  after_save :log_activity
  
  private
  
  def normalize_email
    self.email = email.downcase.strip if email.present?
  end
  
  def set_timestamps
    self.updated_at = Time.current
  end
  
  def set_default_role
    self.role ||= 'user'
  end
  
  def send_welcome_email
    UserMailer.welcome(self).deliver_now
  end
  
  def log_activity
    Rails.logger.info "User #{id} was saved"
  end
end

Update Callbacks

# When you call user.update or user.save on existing record:

# 1. before_validation
# 2. validations
# 3. after_validation
# 4. before_save
# 5. before_update
# 6. UPDATE database
# 7. after_update
# 8. after_save

class User < ApplicationRecord
  before_update :track_changes
  after_update :notify_admin
  
  private
  
  def track_changes
    @previous_email = email_was if email_changed?
  end
  
  def notify_admin
    AdminMailer.user_updated(self, @previous_email).deliver_now if email_changed?
  end
end

Destroy Callbacks

# When you call user.destroy:

# 1. before_destroy
# 2. DELETE FROM database
# 3. after_destroy

class User < ApplicationRecord
  before_destroy :backup_data
  after_destroy :cleanup_files
  
  private
  
  def backup_data
    UserBackup.create!(user_data: attributes)
  end
  
  def cleanup_files
    FileUtils.rm_rf("uploads/users/#{id}")
  end
end
Before Callbacks

Before callbacks run before the main operation and can modify the object or prevent the operation from continuing.

before_validation

class User < ApplicationRecord
  before_validation :normalize_data, :set_defaults
  
  private
  
  def normalize_data
    # Clean up data before validation
    self.email = email.downcase.strip if email.present?
    self.name = name.titleize if name.present?
    self.phone = phone.gsub(/\D/, '') if phone.present?
  end
  
  def set_defaults
    # Set default values
    self.status ||= 'pending'
    self.timezone ||= 'UTC'
  end
end

before_save

class User < ApplicationRecord
  before_save :encrypt_password, :update_timestamps
  
  private
  
  def encrypt_password
    if password_changed?
      self.password_hash = BCrypt::Password.create(password)
    end
  end
  
  def update_timestamps
    self.updated_at = Time.current
    self.last_activity_at = Time.current if status_changed?
  end
end

before_create

class User < ApplicationRecord
  before_create :generate_uuid, :set_creation_ip
  
  private
  
  def generate_uuid
    self.uuid = SecureRandom.uuid
  end
  
  def set_creation_ip
    self.created_from_ip = RequestStore.store[:remote_ip]
  end
end

before_update

class User < ApplicationRecord
  before_update :track_changes, :validate_role_change
  
  private
  
  def track_changes
    @changes = changes.dup
  end
  
  def validate_role_change
    if role_changed? && role_was == 'admin'
      raise "Cannot change admin role"
    end
  end
end

before_destroy

class User < ApplicationRecord
  before_destroy :prevent_admin_deletion, :backup_data
  
  private
  
  def prevent_admin_deletion
    if admin?
      errors.add(:base, "Cannot delete admin users")
      throw(:abort)
    end
  end
  
  def backup_data
    UserBackup.create!(
      user_id: id,
      data: attributes,
      deleted_at: Time.current
    )
  end
end
After Callbacks

After callbacks run after the main operation and are perfect for side effects like sending emails, logging, or updating related records.

after_validation

class User < ApplicationRecord
  after_validation :log_validation_errors
  
  private
  
  def log_validation_errors
    if errors.any?
      Rails.logger.warn "User validation failed: #{errors.full_messages}"
    end
  end
end

after_save

class User < ApplicationRecord
  after_save :update_search_index, :notify_changes
  
  private
  
  def update_search_index
    SearchIndex.update_user(self)
  end
  
  def notify_changes
    if saved_change_to_status?
      UserStatusNotifier.new(self).notify
    end
  end
end

after_create

class User < ApplicationRecord
  after_create :send_welcome_email, :create_profile, :log_registration
  
  private
  
  def send_welcome_email
    UserMailer.welcome(self).deliver_later
  end
  
  def create_profile
    create_profile!(bio: "Welcome to our platform!")
  end
  
  def log_registration
    RegistrationLogger.log(self)
  end
end

after_update

class User < ApplicationRecord
  after_update :notify_admin, :update_related_records
  
  private
  
  def notify_admin
    if email_changed?
      AdminMailer.email_changed(self, email_was).deliver_later
    end
  end
  
  def update_related_records
    posts.update_all(author_name: name) if name_changed?
  end
end

after_destroy

class User < ApplicationRecord
  after_destroy :cleanup_files, :notify_deletion
  
  private
  
  def cleanup_files
    FileUtils.rm_rf("uploads/users/#{id}")
    FileUtils.rm_rf("temp/users/#{id}")
  end
  
  def notify_deletion
    DeletionNotifier.new(self).notify
  end
end
Around Callbacks

Around callbacks wrap the entire operation and give you control over when the operation actually happens.

around_save

class User < ApplicationRecord
  around_save :log_save_time
  
  private
  
  def log_save_time
    start_time = Time.current
    yield  # This is where the save happens
    duration = Time.current - start_time
    Rails.logger.info "User save took #{duration} seconds"
  end
end

around_create

class User < ApplicationRecord
  around_create :wrap_in_transaction
  
  private
  
  def wrap_in_transaction
    ActiveRecord::Base.transaction do
      # Set up any pre-creation data
      self.created_at = Time.current
      yield  # This creates the record
      # Do any post-creation work
      create_initial_data
    end
  end
  
  def create_initial_data
    create_profile!
    create_preferences!
  end
end

around_update

class User < ApplicationRecord
  around_update :track_changes
  
  private
  
  def track_changes
    old_attributes = attributes.dup
    yield  # This updates the record
    new_attributes = attributes.dup
    
    changes = old_attributes.select { |k, v| new_attributes[k] != v }
    ChangeTracker.log(self, changes) if changes.any?
  end
end

around_destroy

class User < ApplicationRecord
  around_destroy :soft_delete_with_backup
  
  private
  
  def soft_delete_with_backup
    # Create backup before deletion
    UserBackup.create!(user_data: attributes)
    
    # Actually delete the record
    yield
    
    # Clean up after deletion
    cleanup_related_data
  end
  
  def cleanup_related_data
    # Clean up any remaining references
    posts.update_all(user_id: nil)
    comments.update_all(user_id: nil)
  end
end
Conditional Callbacks

Conditional callbacks allow you to control when callbacks run based on specific conditions.

Using :if and :unless

class User < ApplicationRecord
  before_save :encrypt_password, if: :password_changed?
  after_save :send_notification, unless: :new_record?
  before_update :validate_role_change, if: -> { role_changed? }
  after_create :send_welcome_email, unless: :skip_welcome_email?
  
  private
  
  def skip_welcome_email?
    email.blank? || test_user?
  end
end

Complex Conditions

class User < ApplicationRecord
  before_save :update_last_login, if: :should_update_last_login?
  after_save :sync_with_external_service, if: :should_sync?
  
  private
  
  def should_update_last_login?
    last_login_at_changed? && !last_login_at_was.nil?
  end
  
  def should_sync?
    (email_changed? || status_changed?) && active?
  end
end

Multiple Conditions

class User < ApplicationRecord
  after_save :send_notification, 
    if: :email_changed?, 
    unless: :test_user?
  
  before_destroy :prevent_deletion,
    if: -> { admin? && User.admin.count <= 1 }
  
  after_update :log_changes,
    if: -> { changes.any? },
    unless: -> { changes.keys == ['updated_at'] }
end
Callback Best Practices

When to Use Callbacks

✅ Good Use Cases:

  • Setting default values
  • Data normalization (email, phone numbers)
  • Simple side effects (logging, notifications)
  • Maintaining data consistency
  • Simple cleanup operations

❌ Avoid Using Callbacks For:

  • Complex business logic
  • External API calls
  • Performance-critical operations
  • Operations that can fail
  • Complex validations

Performance Considerations

class User < ApplicationRecord
  # ✅ Good - Simple, fast operations
  before_save :normalize_email
  after_create :send_welcome_email
  
  # ❌ Bad - Expensive operations in callbacks
  after_save :sync_with_external_api
  after_create :generate_complex_report
  
  private
  
  def normalize_email
    self.email = email.downcase.strip if email.present?
  end
  
  def send_welcome_email
    UserMailer.welcome(self).deliver_later  # Use background jobs
  end
  
  # Move expensive operations to background jobs
  def sync_with_external_api
    ExternalApiSyncJob.perform_later(self)
  end
end

Error Handling

class User < ApplicationRecord
  before_save :encrypt_password
  after_create :send_welcome_email
  
  private
  
  def encrypt_password
    if password_changed?
      self.password_hash = BCrypt::Password.create(password)
    end
  rescue BCrypt::Errors::InvalidPassword
    errors.add(:password, "is invalid")
    throw(:abort)  # Prevent save from continuing
  end
  
  def send_welcome_email
    UserMailer.welcome(self).deliver_later
  rescue => e
    Rails.logger.error "Failed to send welcome email: #{e.message}"
    # Don't throw abort - email failure shouldn't prevent user creation
  end
end

Testing Callbacks

# In your test file
require 'rails_helper'

RSpec.describe User, type: :model do
  describe 'callbacks' do
    it 'normalizes email before save' do
      user = User.create!(name: 'John', email: '[email protected]')
      expect(user.email).to eq('[email protected]')
    end
    
    it 'sends welcome email after create' do
      expect(UserMailer).to receive(:welcome).and_return(
        double(deliver_later: true)
      )
      
      User.create!(name: 'John', email: '[email protected]')
    end
    
    it 'prevents admin deletion' do
      admin = User.create!(name: 'Admin', email: '[email protected]', role: 'admin')
      
      expect { admin.destroy }.not_to change { User.count }
      expect(admin.errors[:base]).to include("Cannot delete admin users")
    end
  end
end
Important: Use throw(:abort) in before callbacks to prevent the operation from continuing, and use background jobs for expensive operations to avoid blocking the main thread.

Queries

ActiveRecord provides a powerful and intuitive query interface for retrieving data from the database. It translates Ruby method calls into SQL queries, making database operations feel natural and Ruby-like.

What are ActiveRecord Queries? Think of queries as Ruby methods that automatically generate SQL for you. Instead of writing raw SQL, you use Ruby methods that ActiveRecord translates into database queries.
What Are Queries and When to Use Them

What Are ActiveRecord Queries?

ActiveRecord queries are methods that allow you to retrieve, filter, and manipulate data from your database tables. They provide a Ruby-like interface to SQL operations, making database interactions more intuitive and maintainable.

Key Concept: Queries translate Ruby method calls into SQL statements and return ActiveRecord objects or relations that you can chain and manipulate.

When to Use Queries

# 1. Finding Specific Records
user = User.find(1)                    # Find by primary key
user = User.find_by(email: '[email protected]')  # Find by attributes
users = User.where(active: true)       # Find multiple records

# 2. Filtering and Searching
recent_users = User.where('created_at > ?', 1.week.ago)
admin_users = User.where(role: 'admin')
verified_users = User.where(verified: true)

# 3. Sorting and Ordering
users = User.order(:name)              # Sort by name
recent_posts = Post.order(created_at: :desc)  # Sort by date descending

# 4. Limiting Results
latest_users = User.order(:created_at).limit(10)
top_products = Product.order(sales_count: :desc).limit(5)

# 5. Aggregating Data
total_users = User.count               # Count all users
avg_age = User.average(:age)           # Calculate average age
total_revenue = Order.sum(:amount)     # Sum order amounts

# 6. Working with Associations
user_posts = user.posts                # Get user's posts
posts_with_comments = Post.includes(:comments)  # Eager load associations

# 7. Complex Conditions
active_admins = User.where(active: true, role: 'admin')
recent_verified = User.where('created_at > ? AND verified = ?', 1.month.ago, true)

# 8. Pagination
users_page_1 = User.limit(20).offset(0)
users_page_2 = User.limit(20).offset(20)

Query Types and Use Cases

Query TypeWhen to UseExampleReturns
Finding RecordsGet specific records by ID or attributesUser.find(1)Single record or array
FilteringGet records matching conditionsUser.where(active: true)Relation (chainable)
SortingOrder results by specific columnsUser.order(:name)Relation (chainable)
LimitingRestrict number of resultsUser.limit(10)Relation (chainable)
AggregatingCalculate sums, averages, countsUser.countNumber
JoiningCombine data from multiple tablesUser.joins(:posts)Relation (chainable)
Eager LoadingPrevent N+1 query problemsUser.includes(:posts)Relation (chainable)
GroupingGroup records for analysisUser.group(:role)Relation (chainable)

Query Chaining and Composition

One of the most powerful features of ActiveRecord queries is method chaining. You can combine multiple query methods to build complex queries:

# Simple chain
User.where(active: true).order(:name).limit(10)

# Complex chain with multiple conditions
User.where(active: true)
    .where('created_at > ?', 1.month.ago)
    .where(verified: true)
    .order(:created_at)
    .limit(20)

# Chain with associations
Post.includes(:user, :comments)
    .where(published: true)
    .where('created_at > ?', 1.week.ago)
    .order(created_at: :desc)
    .limit(10)

# Chain with aggregations
Order.where(status: 'completed')
     .where('created_at >= ?', Date.current.beginning_of_month)
     .group(:user_id)
     .having('SUM(amount) > 1000')

Performance Considerations

Important: Always consider performance when writing queries, especially for large datasets.
# ❌ Bad: N+1 Query Problem
users = User.all
users.each { |user| puts user.posts.count }  # Executes N+1 queries

# ✅ Good: Eager Loading
users = User.includes(:posts)
users.each { |user| puts user.posts.count }  # Executes 2 queries total

# ❌ Bad: Loading all records
User.all.each { |user| process_user(user) }  # Loads all users into memory

# ✅ Good: Batch processing
User.find_each(batch_size: 1000) { |user| process_user(user) }

# ❌ Bad: Multiple queries
users = User.where(active: true)
admins = User.where(role: 'admin')
moderators = User.where(role: 'moderator')

# ✅ Good: Single query with OR
users = User.where(active: true)
            .where(role: ['admin', 'moderator'])

Common Query Patterns

# 1. Authentication and Authorization
current_user = User.find_by(session_token: session[:token])
authorized_users = User.where(role: ['admin', 'moderator'])

# 2. Search and Filtering
search_results = User.where('name ILIKE ? OR email ILIKE ?', "%#{query}%", "%#{query}%")
filtered_products = Product.where(category: category, price: min_price..max_price)

# 3. Dashboard and Analytics
recent_activity = Post.where('created_at > ?', 1.week.ago).order(created_at: :desc)
user_stats = User.group(:role).count
revenue_by_month = Order.group("DATE_TRUNC('month', created_at)").sum(:amount)

# 4. Pagination
page = params[:page] || 1
per_page = 20
users = User.offset((page - 1) * per_page).limit(per_page)

# 5. Data Export
all_users = User.select(:id, :name, :email, :created_at)
                .where(active: true)
                .order(:created_at)

# 6. Bulk Operations
user_ids = User.where(active: false).pluck(:id)
User.where(id: user_ids).update_all(active: true)
Pro Tip: Start with simple queries and gradually build complexity. Always test your queries with real data to ensure they perform well and return the expected results.
Basic Query Methods

Start with these fundamental query methods that form the foundation of ActiveRecord querying.

Finding Records

# Find by primary key
user = User.find(1)
user = User.find([1, 2, 3])  # Find multiple records

# Find by attributes
user = User.find_by(email: '[email protected]')
user = User.find_by!(email: '[email protected]')  # Raises error if not found

# Find first and last
first_user = User.first
last_user = User.last
recent_user = User.order(created_at: :desc).first

# Get all records
all_users = User.all  # Returns ActiveRecord::Relation
users_array = User.all.to_a  # Convert to array

Counting Records

# Count all records
total_users = User.count

# Count with conditions
active_users = User.where(active: true).count
recent_users = User.where('created_at > ?', 1.week.ago).count

# Count distinct values
unique_roles = User.distinct.count(:role)
unique_emails = User.distinct.count(:email)

# Count with grouping
users_by_role = User.group(:role).count
# => {"admin" => 5, "user" => 150, "moderator" => 10}

Checking Existence

# Check if any records exist
if User.exists?
  puts "There are users in the database"
end

# Check if specific record exists
if User.exists?(1)
  puts "User with ID 1 exists"
end

# Check if record with specific attributes exists
if User.exists?(email: '[email protected]')
  puts "User with this email exists"
end

# Check with conditions
if User.where(active: true).exists?
  puts "There are active users"
end
Where Conditions

The `where` method is the most commonly used query method for filtering records based on conditions.

Basic Where Conditions

# Simple equality
active_users = User.where(active: true)
admin_users = User.where(role: 'admin')

# Multiple conditions
admin_active_users = User.where(role: 'admin', active: true)

# String conditions
recent_users = User.where('created_at > ?', 1.week.ago)
users_with_gmail = User.where('email LIKE ?', '%@gmail.com')

# Array conditions
users_in_roles = User.where(role: ['admin', 'moderator'])
users_with_ids = User.where(id: [1, 5, 10, 15])

Advanced Where Conditions

# Range conditions
users_created_this_month = User.where(created_at: 1.month.ago..Time.current)
users_with_age = User.where(age: 18..65)

# NULL conditions
users_without_email = User.where(email: nil)
users_with_email = User.where.not(email: nil)

# NOT conditions
non_admin_users = User.where.not(role: 'admin')
inactive_users = User.where.not(active: true)

# OR conditions
admin_or_moderator = User.where(role: 'admin').or(User.where(role: 'moderator'))

# Complex conditions
premium_users = User.where('subscription_expires_at > ? AND active = ?', 
                          Time.current, true)

String vs Hash Conditions

# Hash conditions (safer, prevents SQL injection)
users = User.where(role: 'admin', active: true)

# String conditions (more flexible, but be careful with user input)
users = User.where('created_at > ? AND email LIKE ?', 
                  1.week.ago, '%@example.com')

# NEVER do this (SQL injection vulnerability)
email = params[:email]
users = User.where("email = '#{email}'")  # DANGEROUS!

# Instead, use parameterized queries
users = User.where('email = ?', email)  # SAFE
Ordering and Limiting

Control the order of results and limit the number of records returned.

Ordering Records

# Simple ordering
recent_users = User.order(created_at: :desc)
alphabetical_users = User.order(:name)
oldest_first = User.order(created_at: :asc)

# Multiple column ordering
users_by_role_and_name = User.order(:role, :name)
users_by_created_and_updated = User.order(created_at: :desc, updated_at: :desc)

# String ordering (for complex expressions)
users_by_name_length = User.order('LENGTH(name) DESC')
users_by_custom_order = User.order("CASE role WHEN 'admin' THEN 1 WHEN 'moderator' THEN 2 ELSE 3 END")

# Random ordering
random_users = User.order('RANDOM()')  # PostgreSQL
random_users = User.order('RAND()')    # MySQL

Limiting and Pagination

# Limit results
recent_5_users = User.order(created_at: :desc).limit(5)

# Offset (skip records)
users_page_2 = User.limit(10).offset(10)  # Records 11-20

# Pagination helper
def paginate(page = 1, per_page = 10)
  limit(per_page).offset((page - 1) * per_page)
end

# Usage
users_page_1 = User.paginate(1, 10)
users_page_2 = User.paginate(2, 10)

# First and last with conditions
first_admin = User.where(role: 'admin').first
last_active_user = User.where(active: true).last

Distinct and Unique

# Remove duplicates
unique_roles = User.distinct.pluck(:role)
unique_emails = User.select(:email).distinct

# Unique combinations
unique_role_status = User.distinct.pluck(:role, :active)

# Count distinct
unique_email_count = User.distinct.count(:email)
Selecting and Plucking

Control which columns are selected and retrieve specific data efficiently.

Selecting Specific Columns

# Select specific columns
users_with_names = User.select(:id, :name, :email)
users_with_names = User.select('id, name, email')

# Select with calculations
users_with_age = User.select(:id, :name, 'YEAR(CURDATE()) - YEAR(birth_date) as age')

# Select with aliases
users_with_full_name = User.select(:id, "CONCAT(first_name, ' ', last_name) as full_name")

# Select with conditions
active_user_ids = User.where(active: true).select(:id)

Plucking Values

# Get single column as array
user_names = User.pluck(:name)
user_emails = User.pluck(:email)

# Get multiple columns as array of arrays
user_data = User.pluck(:id, :name, :email)
# => [[1, "John", "[email protected]"], [2, "Jane", "[email protected]"]]

# Get multiple columns as array of hashes
user_hashes = User.pluck(:id, :name, :email).map { |id, name, email| 
  { id: id, name: name, email: email } 
}

# Pluck with conditions
active_user_names = User.where(active: true).pluck(:name)
admin_emails = User.where(role: 'admin').pluck(:email)

Aggregate Functions

# Count
total_users = User.count
active_count = User.where(active: true).count

# Sum
total_age = User.sum(:age)
total_posts = User.joins(:posts).sum('posts.view_count')

# Average
average_age = User.average(:age)
average_posts_per_user = User.joins(:posts).average('posts.view_count')

# Maximum and Minimum
oldest_user_age = User.maximum(:age)
youngest_user_age = User.minimum(:age)
most_recent_login = User.maximum(:last_login_at)

# Group with aggregates
users_by_role = User.group(:role).count
average_age_by_role = User.group(:role).average(:age)
Joins and Associations

Query across multiple tables using joins and associations.

Basic Joins

# Inner join
users_with_posts = User.joins(:posts)
users_with_comments = User.joins(:comments)

# Multiple joins
users_with_posts_and_comments = User.joins(:posts, :comments)

# Join with conditions
active_users_with_posts = User.joins(:posts).where(active: true)
recent_posts_by_users = User.joins(:posts).where('posts.created_at > ?', 1.week.ago)

# Join with select
users_with_post_count = User.joins(:posts)
                            .select('users.*, COUNT(posts.id) as post_count')
                            .group('users.id')

Left Joins (includes)

# Left outer join (includes users without posts)
all_users_with_posts = User.left_joins(:posts)

# Left join with conditions
users_with_recent_posts = User.left_joins(:posts)
                              .where('posts.created_at > ? OR posts.id IS NULL', 1.week.ago)

# Left join with select
users_with_post_counts = User.left_joins(:posts)
                             .select('users.*, COUNT(posts.id) as post_count')
                             .group('users.id')

# Using includes (eager loading)
users_with_posts = User.includes(:posts)  # Prevents N+1 queries

Complex Joins

# Join through associations
users_with_post_comments = User.joins(posts: :comments)

# Join with custom conditions
users_with_popular_posts = User.joins(:posts)
                               .where('posts.view_count > ?', 1000)

# Join with multiple conditions
active_users_with_recent_posts = User.joins(:posts)
                                    .where(active: true)
                                    .where('posts.created_at > ?', 1.month.ago)

# Join with calculations
users_with_post_stats = User.joins(:posts)
                           .select('users.*, COUNT(posts.id) as total_posts, AVG(posts.view_count) as avg_views')
                           .group('users.id')

Eager Loading

# Prevent N+1 queries
users_with_posts = User.includes(:posts)
users_with_posts_and_comments = User.includes(:posts, :comments)

# Nested includes
users_with_posts_and_comments = User.includes(posts: :comments)

# Preload (alternative to includes)
users_with_posts = User.preload(:posts)

# Eager load with conditions
active_users_with_posts = User.includes(:posts).where(active: true)

# Conditional includes
users_with_recent_posts = User.includes(:posts)
                             .where('posts.created_at > ?', 1.week.ago)
Advanced Query Techniques

Advanced query patterns for complex scenarios.

Subqueries

# Users with more posts than average
users_with_many_posts = User.where('(SELECT COUNT(*) FROM posts WHERE posts.user_id = users.id) > (SELECT AVG(post_count) FROM (SELECT COUNT(*) as post_count FROM posts GROUP BY user_id) as avg_posts)')

# Users who posted this week
users_with_recent_posts = User.where(id: Post.where('created_at > ?', 1.week.ago).select(:user_id))

# Users with highest post count
top_poster = User.joins(:posts)
                 .group('users.id')
                 .order('COUNT(posts.id) DESC')
                 .first

Union Queries

# Combine queries with UNION
admin_users = User.where(role: 'admin')
moderator_users = User.where(role: 'moderator')
staff_users = admin_users.union(moderator_users)

# Union with ordering
all_staff = User.where(role: 'admin')
                .union(User.where(role: 'moderator'))
                .order(:name)

Window Functions

# Rank users by post count
users_with_rank = User.joins(:posts)
                      .select('users.*, ROW_NUMBER() OVER (ORDER BY COUNT(posts.id) DESC) as rank')
                      .group('users.id')

# Users with rank within their role
users_with_role_rank = User.joins(:posts)
                           .select('users.*, ROW_NUMBER() OVER (PARTITION BY role ORDER BY COUNT(posts.id) DESC) as role_rank')
                           .group('users.id')

Dynamic Queries

# Build queries dynamically
def search_users(params = {})
  query = User.all
  
  query = query.where(role: params[:role]) if params[:role].present?
  query = query.where(active: params[:active]) if params[:active].present?
  query = query.where('name LIKE ?', "%#{params[:name]}%") if params[:name].present?
  query = query.where('created_at > ?', params[:since]) if params[:since].present?
  
  query
end

# Usage
recent_active_admins = search_users(role: 'admin', active: true, since: 1.month.ago)
Query Performance

N+1 Query Problem

# ❌ Bad - N+1 queries
users = User.all
users.each do |user|
  puts user.posts.count  # This triggers a separate query for each user
end

# ✅ Good - Use includes or joins
users = User.includes(:posts)
users.each do |user|
  puts user.posts.count  # No additional queries
end

# Alternative with joins
users_with_post_count = User.joins(:posts)
                            .select('users.*, COUNT(posts.id) as post_count')
                            .group('users.id')

Indexing for Performance

# Add indexes for frequently queried columns
class AddIndexesToUsers < ActiveRecord::Migration[7.0]
  def change
    add_index :users, :email, unique: true
    add_index :users, :role
    add_index :users, :active
    add_index :users, :created_at
    add_index :users, [:role, :active]  # Composite index
  end
end

# Use explain to analyze query performance
User.where(role: 'admin').explain

Query Optimization Tips

✅ Performance Best Practices:

  • Use includes to prevent N+1 queries
  • Add indexes on frequently queried columns
  • Use pluck when you only need specific columns
  • Use count instead of length for large datasets
  • Use find_each for processing large datasets

❌ Performance Anti-patterns:

  • Loading all records with User.all for large tables
  • Using each instead of find_each for large datasets
  • Not using indexes on frequently queried columns
  • Using length instead of count for large datasets
Complete Query Reference

Complete reference of all ActiveRecord query methods with descriptions, examples, and usage scenarios.

Query Methods Reference Table

MethodDescriptionSyntaxExampleReturns
findFind by primary key, raises error if not foundfind(id)User.find(1)Record or raises error
find_byFind by attributes, returns nil if not foundfind_by(attributes)User.find_by(email: '[email protected]')Record or nil
find_by!Same as find_by but raises errorfind_by!(attributes)User.find_by!(email: '[email protected]')Record or raises error
firstGet first recordfirstUser.firstRecord or nil
lastGet last recordlastUser.lastRecord or nil
takeGet first n records without orderingtake(limit)User.take(5)Array of records
whereFilter records by conditionswhere(conditions)User.where(active: true)Relation
where.notExclude records by conditionswhere.not(conditions)User.where.not(role: 'admin')Relation
where.orOR logic for conditionswhere.or(conditions)User.where(role: 'admin').or(User.where(role: 'moderator'))Relation
orderSort resultsorder(attributes)User.order(:name)Relation
limitLimit number of resultslimit(number)User.limit(10)Relation
offsetSkip records (pagination)offset(number)User.offset(10)Relation
distinctRemove duplicate recordsdistinctUser.distinctRelation
selectChoose specific columnsselect(columns)User.select(:id, :name, :email)Relation
pluckGet array of valuespluck(columns)User.pluck(:name)Array
idsGet array of primary keysidsUser.idsArray
countCount recordscountUser.countInteger
sumSum values in columnsum(column)Order.sum(:total_amount)Number
averageCalculate averageaverage(column)User.average(:age)Number
maximumFind maximum valuemaximum(column)User.maximum(:age)Value
minimumFind minimum valueminimum(column)User.minimum(:age)Value
joinsINNER JOIN with associationjoins(association)User.joins(:posts)Relation
left_joinsLEFT OUTER JOINleft_joins(association)User.left_joins(:posts)Relation
includesEager loading (prevents N+1)includes(association)User.includes(:posts)Relation
preloadAlternative eager loadingpreload(association)User.preload(:posts)Relation
exists?Check if records existexists?User.exists?Boolean
any?Check if relation has recordsany?User.any?Boolean
none?Check if relation is emptynone?User.none?Boolean
groupGroup records by columngroup(column)User.group(:role)Relation
havingFilter grouped resultshaving(conditions)Post.group(:user_id).having('COUNT(*) > 5')Relation

Finding Records

# find(id) - Find by primary key, raises error if not found
user = User.find(1)
user = User.find([1, 2, 3])  # Multiple records

# find_by(attributes) - Find by attributes, returns nil if not found
user = User.find_by(email: '[email protected]')
user = User.find_by(email: '[email protected]', active: true)

# find_by!(attributes) - Same as find_by but raises error
user = User.find_by!(email: '[email protected]')

# first - Get first record
user = User.first
user = User.where(active: true).first

# last - Get last record
user = User.last
user = User.order(:created_at).last

# take(limit) - Get first n records without ordering
users = User.take(5)

Where Conditions

# where(conditions) - Most common query method
users = User.where(active: true)
users = User.where(role: 'admin', active: true)
users = User.where('created_at > ?', 1.week.ago)
users = User.where(role: ['admin', 'moderator'])
users = User.where(created_at: 1.month.ago..Time.current)

# where.not(conditions) - Exclude records
users = User.where.not(role: 'admin')
users = User.where.not('email LIKE ?', '%@gmail.com')

# where.or(conditions) - OR logic
users = User.where(role: 'admin').or(User.where(role: 'moderator'))

Ordering and Limiting

# order(attributes) - Sort results
users = User.order(:name)
users = User.order(created_at: :desc)
users = User.order(:role, :name)

# limit(number) - Limit results
users = User.limit(10)
users = User.order(:created_at).limit(5)

# offset(number) - Pagination
users = User.limit(10).offset(10)  # Page 2

# distinct - Remove duplicates
users = User.distinct.pluck(:role)

Selecting and Plucking

# select(columns) - Choose specific columns
users = User.select(:id, :name, :email)
users = User.select('id, name, email')

# pluck(columns) - Get arrays of values
names = User.pluck(:name)
user_data = User.pluck(:id, :name, :email)

# ids - Get primary key arrays
user_ids = User.ids
active_user_ids = User.where(active: true).ids

Aggregate Functions

# count - Count records
total = User.count
active_count = User.where(active: true).count
users_by_role = User.group(:role).count

# sum(column) - Sum values
total_age = User.sum(:age)
total_revenue = Order.where(status: 'paid').sum(:total_amount)

# average(column) - Calculate averages
avg_age = User.average(:age)
avg_order = Order.average(:total_amount)

# maximum(column) - Find maximum values
max_age = User.maximum(:age)
max_order = Order.maximum(:total_amount)

# minimum(column) - Find minimum values
min_age = User.minimum(:age)
min_order = Order.minimum(:total_amount)

Joins and Associations

# joins(association) - INNER JOIN
users_with_posts = User.joins(:posts)
users_with_orders = User.joins(:orders)

# left_joins(association) - LEFT OUTER JOIN
all_users_with_posts = User.left_joins(:posts)

# includes(association) - Eager loading (prevents N+1)
users_with_posts = User.includes(:posts)
posts_with_authors = Post.includes(:user)

# preload(association) - Alternative eager loading
users_with_posts = User.preload(:posts)

Existence and Validation

# exists? - Check if records exist
if User.exists?
  puts "There are users"
end

if User.where(active: true).exists?
  puts "There are active users"
end

# any? - Check if relation has records
if User.any?
  puts "Users exist"
end

# none? - Check if relation is empty
if User.where(active: true).none?
  puts "No active users"
end

Grouping and Having

# group(column) - Group records
users_by_role = User.group(:role).count
posts_by_month = Post.group("DATE_TRUNC('month', created_at)")

# having(conditions) - Filter grouped results
popular_posts = Post.joins(:likes)
                   .group('posts.id')
                   .having('COUNT(likes.id) > 5')

high_value_orders = Order.group(:user_id)
                        .having('SUM(total_amount) > 1000')
Important: Always use parameterized queries to prevent SQL injection, and use includes or joins to avoid N+1 query problems.

Custom Models for Business Logic

Custom models that don't map to database tables but encapsulate complex business logic, calculations, and data transformations.

Business Logic Models: These are Ruby classes that use ActiveRecord patterns but don't persist to the database. They're perfect for complex calculations, data processing, and business rule encapsulation.
Service Objects

Service objects encapsulate complex business logic and operations that don't belong in models or controllers.

User Registration Service

class UserRegistrationService
  include ActiveModel::Model
  include ActiveModel::Validations

  attr_accessor :email, :password, :password_confirmation, :terms_accepted
  attr_reader :user

  validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :password, presence: true, length: { minimum: 8 }
  validates :password_confirmation, presence: true
  validates :terms_accepted, acceptance: true
  validate :passwords_match

  def initialize(attributes = {})
    super
    @user = nil
  end

  def call
    return false unless valid?

    ActiveRecord::Base.transaction do
      @user = User.create!(
        email: email.downcase.strip,
        password: password,
        password_confirmation: password_confirmation
      )

      create_user_profile
      send_welcome_email
      track_registration_event
      
      true
    rescue ActiveRecord::RecordInvalid => e
      errors.merge!(e.record.errors)
      false
    end
  end

  private

  def passwords_match
    return if password.blank? || password_confirmation.blank?
    
    if password != password_confirmation
      errors.add(:password_confirmation, "doesn't match password")
    end
  end

  def create_user_profile
    @user.create_profile!(
      email_preferences: { marketing: true, updates: true },
      timezone: 'UTC'
    )
  end

  def send_welcome_email
    UserMailer.welcome(@user).deliver_later
  end

  def track_registration_event
    AnalyticsService.track('user_registered', user_id: @user.id)
  end
end

# Usage in controller
class RegistrationsController < ApplicationController
  def create
    service = UserRegistrationService.new(user_params)
    
    if service.call
      redirect_to dashboard_path, notice: 'Account created successfully!'
    else
      render :new, locals: { service: service }
    end
  end

  private

  def user_params
    params.require(:user_registration).permit(:email, :password, :password_confirmation, :terms_accepted)
  end
end

Order Processing Service

class OrderProcessingService
  include ActiveModel::Model
  include ActiveModel::Validations

  attr_accessor :user, :items, :shipping_address, :payment_method
  attr_reader :order, :errors

  validates :user, presence: true
  validates :items, presence: true
  validates :shipping_address, presence: true
  validates :payment_method, presence: true
  validate :items_available
  validate :user_can_afford

  def initialize(attributes = {})
    super
    @order = nil
    @errors = []
  end

  def call
    return false unless valid?

    ActiveRecord::Base.transaction do
      create_order
      process_payment
      update_inventory
      send_notifications
      
      true
    rescue => e
      @errors << e.message
      false
    end
  end

  def total_amount
    items.sum { |item| item[:quantity] * Product.find(item[:product_id]).price }
  end

  def tax_amount
    total_amount * 0.08 # 8% tax
  end

  def shipping_amount
    calculate_shipping_cost
  end

  def final_total
    total_amount + tax_amount + shipping_amount
  end

  private

  def create_order
    @order = Order.create!(
      user: user,
      shipping_address: shipping_address,
      total_amount: final_total,
      tax_amount: tax_amount,
      shipping_amount: shipping_amount,
      status: 'pending'
    )

    items.each do |item|
      product = Product.find(item[:product_id])
      @order.order_items.create!(
        product: product,
        quantity: item[:quantity],
        unit_price: product.price
      )
    end
  end

  def process_payment
    case payment_method
    when 'credit_card'
      PaymentProcessor.charge(@order.total_amount, user.credit_card)
    when 'paypal'
      PayPalService.process_payment(@order)
    end
  end

  def update_inventory
    @order.order_items.each do |item|
      item.product.update!(stock_quantity: item.product.stock_quantity - item.quantity)
    end
  end

  def send_notifications
    OrderMailer.confirmation(@order).deliver_later
    AdminMailer.new_order(@order).deliver_later
  end

  def items_available
    items.each do |item|
      product = Product.find(item[:product_id])
      if product.stock_quantity < item[:quantity]
        errors.add(:items, "#{product.name} is out of stock")
      end
    end
  end

  def user_can_afford
    if user.balance < final_total
      errors.add(:base, "Insufficient funds")
    end
  end

  def calculate_shipping_cost
    # Complex shipping calculation logic
    base_cost = 10.0
    weight_cost = items.sum { |item| Product.find(item[:product_id]).weight * item[:quantity] } * 0.5
    distance_cost = ShippingCalculator.calculate_distance(shipping_address) * 0.1
    
    base_cost + weight_cost + distance_cost
  end
end
Value Objects

Value objects represent concepts that have no identity and are defined by their attributes.

Money Value Object

class Money
  include ActiveModel::Model
  include ActiveModel::Validations

  attr_accessor :amount, :currency

  validates :amount, presence: true, numericality: { greater_than_or_equal_to: 0 }
  validates :currency, presence: true, inclusion: { in: %w[USD EUR GBP] }

  def initialize(amount, currency = 'USD')
    @amount = amount.to_f
    @currency = currency.upcase
  end

  def +(other)
    raise ArgumentError, "Cannot add different currencies" unless currency == other.currency
    Money.new(amount + other.amount, currency)
  end

  def -(other)
    raise ArgumentError, "Cannot subtract different currencies" unless currency == other.currency
    Money.new(amount - other.amount, currency)
  end

  def *(multiplier)
    Money.new(amount * multiplier, currency)
  end

  def /(divisor)
    Money.new(amount / divisor, currency)
  end

  def ==(other)
    amount == other.amount && currency == other.currency
  end

  def to_s
    "#{currency} #{format('%.2f', amount)}"
  end

  def zero?
    amount.zero?
  end

  def positive?
    amount > 0
  end

  def negative?
    amount < 0
  end

  def convert_to(target_currency)
    return self if currency == target_currency
    
    rate = ExchangeRateService.get_rate(currency, target_currency)
    Money.new(amount * rate, target_currency)
  end
end

# Usage in models
class Product < ApplicationRecord
  def price
    Money.new(price_cents / 100.0, price_currency)
  end

  def price=(money)
    self.price_cents = (money.amount * 100).to_i
    self.price_currency = money.currency
  end
end

# Usage examples
price = Money.new(29.99, 'USD')
tax = price * 0.08
total = price + tax
puts total # => "USD 32.39"

Address Value Object

class Address
  include ActiveModel::Model
  include ActiveModel::Validations

  attr_accessor :street, :city, :state, :postal_code, :country

  validates :street, presence: true
  validates :city, presence: true
  validates :state, presence: true
  validates :postal_code, presence: true
  validates :country, presence: true
  validate :postal_code_format

  def initialize(attributes = {})
    super
    @country ||= 'US'
  end

  def full_address
    [street, city, state, postal_code, country].compact.join(', ')
  end

  def shipping_zone
    case country
    when 'US'
      case state
      when /^(CA|OR|WA)$/ then 'west_coast'
      when /^(NY|NJ|CT|MA|RI|VT|NH|ME)$/ then 'northeast'
      else 'continental'
      end
    when 'CA' then 'canada'
    else 'international'
    end
  end

  def shipping_cost
    case shipping_zone
    when 'west_coast', 'northeast' then Money.new(5.99, 'USD')
    when 'continental' then Money.new(7.99, 'USD')
    when 'canada' then Money.new(12.99, 'USD')
    else Money.new(19.99, 'USD')
    end
  end

  def ==(other)
    street == other.street &&
    city == other.city &&
    state == other.state &&
    postal_code == other.postal_code &&
    country == other.country
  end

  private

  def postal_code_format
    case country
    when 'US'
      unless postal_code.match?(/^\d{5}(-\d{4})?$/)
        errors.add(:postal_code, 'must be in US format (12345 or 12345-6789)')
      end
    when 'CA'
      unless postal_code.match?(/^[A-Z]\d[A-Z] \d[A-Z]\d$/)
        errors.add(:postal_code, 'must be in Canadian format (A1A 1A1)')
      end
    end
  end
end

# Usage in models
class User < ApplicationRecord
  def shipping_address
    Address.new(
      street: shipping_street,
      city: shipping_city,
      state: shipping_state,
      postal_code: shipping_postal_code,
      country: shipping_country
    )
  end

  def shipping_address=(address)
    self.shipping_street = address.street
    self.shipping_city = address.city
    self.shipping_state = address.state
    self.shipping_postal_code = address.postal_code
    self.shipping_country = address.country
  end
end
Form Objects

Form objects handle complex form submissions that involve multiple models or complex validations.

User Registration Form

class UserRegistrationForm
  include ActiveModel::Model
  include ActiveModel::Validations

  attr_accessor :email, :password, :password_confirmation, :first_name, :last_name,
                :company_name, :phone, :newsletter_subscription, :terms_accepted

  validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :password, presence: true, length: { minimum: 8 }
  validates :password_confirmation, presence: true
  validates :first_name, presence: true, length: { minimum: 2 }
  validates :last_name, presence: true, length: { minimum: 2 }
  validates :phone, format: { with: /\A\+?[\d\s\-\(\)]+\z/ }, allow_blank: true
  validates :terms_accepted, acceptance: true
  validate :passwords_match
  validate :email_uniqueness

  def initialize(attributes = {})
    super
    @newsletter_subscription ||= false
  end

  def save
    return false unless valid?

    ActiveRecord::Base.transaction do
      create_user
      create_profile
      create_preferences
      send_welcome_email
      
      true
    rescue ActiveRecord::RecordInvalid => e
      errors.merge!(e.record.errors)
      false
    end
  end

  def user
    @user ||= User.new(user_attributes)
  end

  def profile
    @profile ||= user.build_profile(profile_attributes)
  end

  def preferences
    @preferences ||= user.build_preferences(preferences_attributes)
  end

  private

  def user_attributes
    {
      email: email.downcase.strip,
      password: password,
      password_confirmation: password_confirmation,
      first_name: first_name.strip,
      last_name: last_name.strip
    }
  end

  def profile_attributes
    {
      company_name: company_name&.strip,
      phone: phone&.strip,
      bio: "#{first_name} #{last_name} from #{company_name}"
    }
  end

  def preferences_attributes
    {
      newsletter_subscription: newsletter_subscription,
      email_notifications: true,
      marketing_emails: newsletter_subscription
    }
  end

  def create_user
    @user = User.create!(user_attributes)
  end

  def create_profile
    @user.create_profile!(profile_attributes)
  end

  def create_preferences
    @user.create_preferences!(preferences_attributes)
  end

  def send_welcome_email
    UserMailer.welcome(@user).deliver_later
  end

  def passwords_match
    return if password.blank? || password_confirmation.blank?
    
    if password != password_confirmation
      errors.add(:password_confirmation, "doesn't match password")
    end
  end

  def email_uniqueness
    if User.exists?(email: email&.downcase&.strip)
      errors.add(:email, 'has already been taken')
    end
  end
end

# Usage in controller
class RegistrationsController < ApplicationController
  def new
    @form = UserRegistrationForm.new
  end

  def create
    @form = UserRegistrationForm.new(form_params)
    
    if @form.save
      redirect_to dashboard_path, notice: 'Account created successfully!'
    else
      render :new
    end
  end

  private

  def form_params
    params.require(:user_registration_form).permit(
      :email, :password, :password_confirmation, :first_name, :last_name,
      :company_name, :phone, :newsletter_subscription, :terms_accepted
    )
  end
end

Order Checkout Form

class OrderCheckoutForm
  include ActiveModel::Model
  include ActiveModel::Validations

  attr_accessor :user, :items, :shipping_address, :billing_address,
                :payment_method, :card_number, :expiry_date, :cvv,
                :coupon_code, :gift_message

  validates :user, presence: true
  validates :items, presence: true
  validates :shipping_address, presence: true
  validates :payment_method, presence: true, inclusion: { in: %w[credit_card paypal] }
  validates :card_number, presence: true, if: :credit_card?
  validates :expiry_date, presence: true, if: :credit_card?
  validates :cvv, presence: true, if: :credit_card?
  validate :items_available
  validate :valid_coupon
  validate :shipping_address_valid

  def initialize(attributes = {})
    super
    @coupon = nil
  end

  def save
    return false unless valid?

    ActiveRecord::Base.transaction do
      create_order
      apply_coupon if coupon_code.present?
      process_payment
      update_inventory
      send_notifications
      
      true
    rescue => e
      errors.add(:base, e.message)
      false
    end
  end

  def subtotal
    items.sum { |item| item[:quantity] * Product.find(item[:product_id]).price }
  end

  def tax_amount
    subtotal * tax_rate
  end

  def shipping_amount
    calculate_shipping_cost
  end

  def discount_amount
    return Money.new(0, 'USD') unless @coupon
    
    case @coupon.discount_type
    when 'percentage'
      Money.new(subtotal.amount * (@coupon.discount_value / 100.0), 'USD')
    when 'fixed'
      Money.new(@coupon.discount_value, 'USD')
    end
  end

  def total_amount
    subtotal + tax_amount + shipping_amount - discount_amount
  end

  def coupon
    @coupon
  end

  private

  def create_order
    @order = Order.create!(
      user: user,
      shipping_address: shipping_address,
      billing_address: billing_address || shipping_address,
      subtotal: subtotal,
      tax_amount: tax_amount,
      shipping_amount: shipping_amount,
      discount_amount: discount_amount,
      total_amount: total_amount,
      status: 'pending',
      gift_message: gift_message
    )

    items.each do |item|
      product = Product.find(item[:product_id])
      @order.order_items.create!(
        product: product,
        quantity: item[:quantity],
        unit_price: product.price
      )
    end
  end

  def apply_coupon
    @coupon = Coupon.find_by(code: coupon_code, active: true)
    @order.update!(coupon: @coupon, discount_amount: discount_amount)
  end

  def process_payment
    case payment_method
    when 'credit_card'
      PaymentProcessor.charge(total_amount, card_attributes)
    when 'paypal'
      PayPalService.process_payment(@order)
    end
  end

  def update_inventory
    @order.order_items.each do |item|
      item.product.update!(stock_quantity: item.product.stock_quantity - item.quantity)
    end
  end

  def send_notifications
    OrderMailer.confirmation(@order).deliver_later
    AdminMailer.new_order(@order).deliver_later
  end

  def items_available
    items.each do |item|
      product = Product.find(item[:product_id])
      if product.stock_quantity < item[:quantity]
        errors.add(:items, "#{product.name} is out of stock")
      end
    end
  end

  def valid_coupon
    return unless coupon_code.present?
    
    @coupon = Coupon.find_by(code: coupon_code, active: true)
    unless @coupon
      errors.add(:coupon_code, 'is invalid')
      return
    end

    unless @coupon.valid_for_user?(user)
      errors.add(:coupon_code, 'cannot be used by this user')
    end
  end

  def shipping_address_valid
    return unless shipping_address
    
    unless shipping_address.valid?
      errors.add(:shipping_address, 'is invalid')
    end
  end

  def credit_card?
    payment_method == 'credit_card'
  end

  def card_attributes
    {
      number: card_number,
      expiry_date: expiry_date,
      cvv: cvv
    }
  end

  def tax_rate
    case shipping_address&.state
    when 'CA' then 0.085
    when 'NY' then 0.08
    else 0.06
    end
  end

  def calculate_shipping_cost
    # Complex shipping calculation
    base_cost = Money.new(5.99, 'USD')
    weight_cost = items.sum { |item| Product.find(item[:product_id]).weight * item[:quantity] } * 0.5
    distance_cost = ShippingCalculator.calculate_distance(shipping_address) * 0.1
    
    Money.new(base_cost.amount + weight_cost + distance_cost, 'USD')
  end
end
Query Objects

Query objects encapsulate complex database queries and can be reused across the application.

User Search Query

class UserSearchQuery
  include ActiveModel::Model

  attr_accessor :query, :role, :status, :date_range, :sort_by, :sort_direction

  def initialize(attributes = {})
    super
    @sort_by ||= 'created_at'
    @sort_direction ||= 'desc'
  end

  def call
    users = User.all
    users = apply_search_filter(users)
    users = apply_role_filter(users)
    users = apply_status_filter(users)
    users = apply_date_filter(users)
    users = apply_sorting(users)
    users
  end

  def results
    @results ||= call
  end

  def count
    results.count
  end

  def paginated(page = 1, per_page = 20)
    results.page(page).per(per_page)
  end

  private

  def apply_search_filter(users)
    return users unless query.present?
    
    users.where(
      'username ILIKE :query OR email ILIKE :query OR first_name ILIKE :query OR last_name ILIKE :query',
      query: "%#{query}%"
    )
  end

  def apply_role_filter(users)
    return users unless role.present?
    users.where(role: role)
  end

  def apply_status_filter(users)
    return users unless status.present?
    
    case status
    when 'active'
      users.where(active: true)
    when 'inactive'
      users.where(active: false)
    when 'verified'
      users.where(verified: true)
    when 'unverified'
      users.where(verified: false)
    else
      users
    end
  end

  def apply_date_filter(users)
    return users unless date_range.present?
    
    case date_range
    when 'today'
      users.where('created_at >= ?', Date.current.beginning_of_day)
    when 'this_week'
      users.where('created_at >= ?', Date.current.beginning_of_week)
    when 'this_month'
      users.where('created_at >= ?', Date.current.beginning_of_month)
    when 'last_month'
      users.where(created_at: 1.month.ago.beginning_of_month..1.month.ago.end_of_month)
    when 'custom'
      # Handle custom date range
      users
    else
      users
    end
  end

  def apply_sorting(users)
    direction = sort_direction == 'desc' ? 'desc' : 'asc'
    users.order("#{sort_by} #{direction}")
  end
end

# Usage in controller
class Admin::UsersController < ApplicationController
  def index
    @search = UserSearchQuery.new(search_params)
    @users = @search.paginated(params[:page])
  end

  private

  def search_params
    params.permit(:query, :role, :status, :date_range, :sort_by, :sort_direction)
  end
end

Product Recommendation Query

class ProductRecommendationQuery
  include ActiveModel::Model

  attr_accessor :user, :category, :limit, :exclude_purchased

  def initialize(attributes = {})
    super
    @limit ||= 10
    @exclude_purchased ||= true
  end

  def call
    products = Product.active.includes(:category, :reviews)
    products = apply_user_preferences(products)
    products = apply_category_filter(products)
    products = exclude_purchased_products(products) if exclude_purchased
    products = apply_ranking(products)
    products.limit(limit)
  end

  def personalized_recommendations
    return call unless user
    
    # Mix of different recommendation strategies
    collaborative = collaborative_filtering
    content_based = content_based_filtering
    popular = popular_in_category
    
    (collaborative + content_based + popular).uniq.first(limit)
  end

  def trending_products
    Product.joins(:order_items)
           .where('order_items.created_at >= ?', 1.week.ago)
           .group('products.id')
           .order('COUNT(order_items.id) DESC')
           .limit(limit)
  end

  def similar_products(product)
    Product.joins(:category)
           .where(category: product.category)
           .where.not(id: product.id)
           .where('price BETWEEN ? AND ?', product.price * 0.7, product.price * 1.3)
           .limit(limit)
  end

  private

  def apply_user_preferences(products)
    return products unless user
    
    if user.preferences&.favorite_categories.present?
      products = products.where(category: user.preferences.favorite_categories)
    end
    
    if user.preferences&.price_range.present?
      min_price, max_price = user.preferences.price_range
      products = products.where(price: min_price..max_price)
    end
    
    products
  end

  def apply_category_filter(products)
    return products unless category.present?
    products.where(category: category)
  end

  def exclude_purchased_products(products)
    return products unless user
    
    purchased_product_ids = user.orders.joins(:order_items)
                               .pluck('order_items.product_id')
                               .uniq
    
    products.where.not(id: purchased_product_ids)
  end

  def apply_ranking(products)
    products.joins(:reviews)
            .group('products.id')
            .order('AVG(reviews.rating) DESC, COUNT(reviews.id) DESC')
  end

  def collaborative_filtering
    # Find users with similar purchase history
    similar_users = find_similar_users
    return [] if similar_users.empty?
    
    Product.joins(order_items: :order)
           .where(orders: { user_id: similar_users })
           .where.not(id: user.purchased_product_ids)
           .group('products.id')
           .order('COUNT(orders.id) DESC')
           .limit(limit / 2)
  end

  def content_based_filtering
    # Based on user's purchase history categories and brands
    favorite_categories = user.purchased_products.joins(:category)
                             .group('categories.id')
                             .order('COUNT(products.id) DESC')
                             .limit(3)
    
    Product.joins(:category)
           .where(category: favorite_categories)
           .where.not(id: user.purchased_product_ids)
           .limit(limit / 2)
  end

  def popular_in_category
    Product.joins(:category)
           .where(category: category)
           .joins(:reviews)
           .group('products.id')
           .order('AVG(reviews.rating) DESC')
           .limit(limit / 2)
  end

  def find_similar_users
    user_purchased_categories = user.purchased_products.joins(:category).pluck('categories.id')
    
    User.joins(orders: { order_items: { product: :category } })
        .where(categories: { id: user_purchased_categories })
        .where.not(id: user.id)
        .group('users.id')
        .having('COUNT(DISTINCT categories.id) >= ?', user_purchased_categories.size / 2)
        .limit(10)
  end
end

# Usage
recommendations = ProductRecommendationQuery.new(
  user: current_user,
  category: Category.find_by(name: 'Electronics'),
  limit: 5
).personalized_recommendations
Key Benefits: Custom models help separate concerns, make code more testable, and encapsulate complex business logic. They follow ActiveRecord patterns without database persistence.

Enhanced Real-World Examples

Advanced examples demonstrating ActiveRecord in complex real-world scenarios with practical business logic.

Real-World Applications: These examples show how ActiveRecord features work together in complex business scenarios, from e-commerce to social media platforms.
E-commerce Inventory Management

Complete e-commerce inventory management system with products, variants, stock tracking, and automated alerts.

Product Model with Variants

class Product < ApplicationRecord
  has_many :variants, dependent: :destroy
  has_many :categories, through: :product_categories
  has_many :product_categories, dependent: :destroy
  has_many :reviews, dependent: :destroy
  has_many :order_items, through: :variants
  
  # Enums
  enum status: { draft: 0, active: 1, archived: 2, discontinued: 3 }
  enum visibility: { hidden: 0, visible: 1, featured: 2 }
  
  # Validations
  validates :name, presence: true, length: { minimum: 2, maximum: 100 }
  validates :sku, presence: true, uniqueness: true
  validates :price, presence: true, numericality: { greater_than: 0 }
  validates :weight, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
  validates :description, length: { maximum: 2000 }
  
  # Scopes
  scope :active, -> { where(status: :active, visibility: :visible) }
  scope :featured, -> { where(visibility: :featured) }
  scope :in_stock, -> { joins(:variants).where('variants.stock_quantity > 0').distinct }
  scope :low_stock, -> { joins(:variants).where('variants.stock_quantity <= variants.reorder_point').distinct }
  scope :out_of_stock, -> { joins(:variants).where('variants.stock_quantity = 0').distinct }
  scope :by_category, ->(category) { joins(:categories).where(categories: { id: category.id }) }
  scope :by_price_range, ->(min, max) { where(price: min..max) }
  scope :recently_added, -> { where('created_at >= ?', 30.days.ago) }
  
  # Callbacks
  before_create :generate_sku
  before_save :update_search_index
  after_save :notify_low_stock, if: :saved_change_to_status?
  
  # Custom methods
  def total_stock
    variants.sum(:stock_quantity)
  end
  
  def has_stock?
    variants.where('stock_quantity > 0').exists?
  end
  
  def low_stock?
    variants.where('stock_quantity <= reorder_point').exists?
  end
  
  def average_rating
    reviews.average(:rating)&.round(1) || 0
  end
  
  def total_sales
    order_items.joins(:order).where(orders: { status: 'completed' }).sum(:quantity)
  end
  
  def revenue
    order_items.joins(:order).where(orders: { status: 'completed' }).sum('order_items.quantity * order_items.unit_price')
  end
  
  def discount_price
    return price unless discount_percentage&.positive?
    price * (1 - discount_percentage / 100.0)
  end
  
  def on_sale?
    discount_percentage&.positive?
  end
  
  private
  
  def generate_sku
    return if sku.present?
    
    base_sku = name.parameterize.upcase[0..5]
    counter = 1
    
    loop do
      new_sku = "#{base_sku}#{counter.to_s.rjust(3, '0')}"
      unless Product.exists?(sku: new_sku)
        self.sku = new_sku
        break
      end
      counter += 1
    end
  end
  
  def update_search_index
    self.search_vector = "#{name} #{description} #{categories.pluck(:name).join(' ')}"
  end
  
  def notify_low_stock
    return unless low_stock?
    
    AdminMailer.low_stock_alert(self).deliver_later
    SlackNotifier.low_stock_alert(self).deliver_later
  end
end

Variant Model with Stock Management

class Variant < ApplicationRecord
  belongs_to :product
  has_many :order_items, dependent: :destroy
  has_many :orders, through: :order_items
  
  # Enums
  enum status: { active: 0, inactive: 1, discontinued: 2 }
  
  # Validations
  validates :sku, presence: true, uniqueness: true
  validates :price, presence: true, numericality: { greater_than: 0 }
  validates :stock_quantity, numericality: { greater_than_or_equal_to: 0 }
  validates :reorder_point, numericality: { greater_than_or_equal_to: 0 }
  
  # Scopes
  scope :in_stock, -> { where('stock_quantity > 0') }
  scope :low_stock, -> { where('stock_quantity <= reorder_point') }
  scope :out_of_stock, -> { where(stock_quantity: 0) }
  scope :needs_reorder, -> { where('stock_quantity <= reorder_point AND auto_reorder = ?', true) }
  
  # Callbacks
  before_create :generate_variant_sku
  after_update :check_reorder_point, if: :saved_change_to_stock_quantity?
  
  # Custom methods
  def available?
    stock_quantity > 0 && status == 'active'
  end
  
  def low_stock?
    stock_quantity <= reorder_point
  end
  
  def stock_percentage
    return 0 if max_stock_quantity.zero?
    (stock_quantity.to_f / max_stock_quantity * 100).round(1)
  end
  
  def reserve_stock(quantity)
    return false if stock_quantity < quantity
    
    update!(stock_quantity: stock_quantity - quantity)
    true
  end
  
  def release_stock(quantity)
    update!(stock_quantity: stock_quantity + quantity)
  end
  
  def reorder_quantity
    max_stock_quantity - stock_quantity
  end
  
  private
  
  def generate_variant_sku
    return if sku.present?
    
    base_sku = product.sku
    variant_sku = "#{base_sku}-#{SecureRandom.hex(3).upcase}"
    self.sku = variant_sku
  end
  
  def check_reorder_point
    return unless low_stock? && auto_reorder?
    
    ReorderJob.perform_later(self)
    AdminMailer.reorder_alert(self).deliver_later
  end
end

Inventory Management Service

class InventoryManagementService
  include ActiveModel::Model
  
  attr_accessor :variant, :quantity, :operation, :reason
  
  validates :variant, presence: true
  validates :quantity, presence: true, numericality: { greater_than: 0 }
  validates :operation, presence: true, inclusion: { in: %w[add remove reserve release] }
  validates :reason, presence: true
  
  def call
    return false unless valid?
    
    ActiveRecord::Base.transaction do
      case operation
      when 'add'
        add_stock
      when 'remove'
        remove_stock
      when 'reserve'
        reserve_stock
      when 'release'
        release_stock
      end
      
      create_inventory_log
      check_alerts
      
      true
    rescue => e
      errors.add(:base, e.message)
      false
    end
  end
  
  private
  
  def add_stock
    variant.update!(
      stock_quantity: variant.stock_quantity + quantity,
      last_restocked_at: Time.current
    )
  end
  
  def remove_stock
    if variant.stock_quantity < quantity
      raise "Insufficient stock. Available: #{variant.stock_quantity}, Requested: #{quantity}"
    end
    
    variant.update!(stock_quantity: variant.stock_quantity - quantity)
  end
  
  def reserve_stock
    if variant.stock_quantity < quantity
      raise "Insufficient stock for reservation"
    end
    
    variant.update!(stock_quantity: variant.stock_quantity - quantity)
  end
  
  def release_stock
    variant.update!(stock_quantity: variant.stock_quantity + quantity)
  end
  
  def create_inventory_log
    InventoryLog.create!(
      variant: variant,
      quantity: quantity,
      operation: operation,
      reason: reason,
      previous_quantity: variant.stock_quantity_was,
      new_quantity: variant.stock_quantity,
      performed_by: Current.user
    )
  end
  
  def check_alerts
    if variant.low_stock?
      LowStockAlertJob.perform_later(variant)
    end
    
    if variant.stock_quantity.zero?
      OutOfStockAlertJob.perform_later(variant)
    end
  end
end

# Usage
service = InventoryManagementService.new(
  variant: variant,
  quantity: 50,
  operation: 'add',
  reason: 'Restock from supplier'
)

if service.call
  puts "Stock updated successfully"
else
  puts "Error: #{service.errors.full_messages.join(', ')}"
end
User Authentication & Authorization

Comprehensive user authentication and authorization system with roles, permissions, and security features.

User Model with Authentication

class User < ApplicationRecord
  has_secure_password
  
  has_many :posts, dependent: :destroy
  has_many :comments, dependent: :destroy
  has_many :likes, dependent: :destroy
  has_one :profile, dependent: :destroy
  has_many :user_roles, dependent: :destroy
  has_many :roles, through: :user_roles
  has_many :permissions, through: :roles
  has_many :sessions, dependent: :destroy
  has_many :login_attempts, dependent: :destroy
  
  # Enums
  enum status: { pending: 0, active: 1, suspended: 2, banned: 3 }
  enum verification_status: { unverified: 0, email_verified: 1, phone_verified: 2, fully_verified: 3 }
  
  # Validations
  validates :email, presence: true, uniqueness: true, 
            format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :username, presence: true, uniqueness: true,
            length: { minimum: 3, maximum: 30 },
            format: { with: /\A[a-zA-Z0-9_]+\z/ }
  validates :password, length: { minimum: 8 }, if: :password_required?
  validates :age, numericality: { greater_than: 13 }, allow_nil: true
  
  # Scopes
  scope :active, -> { where(status: :active) }
  scope :verified, -> { where.not(verification_status: :unverified) }
  scope :recent, -> { where('created_at >= ?', 30.days.ago) }
  scope :by_role, ->(role_name) { joins(:roles).where(roles: { name: role_name }) }
  scope :with_permission, ->(permission_name) { 
    joins(roles: :permissions).where(permissions: { name: permission_name }).distinct 
  }
  
  # Callbacks
  before_create :set_default_role
  before_save :normalize_email
  after_create :create_profile
  after_create :send_welcome_email
  after_update :log_status_change, if: :saved_change_to_status?
  
  # Custom methods
  def admin?
    roles.exists?(name: 'admin')
  end
  
  def moderator?
    roles.exists?(name: 'moderator')
  end
  
  def has_permission?(permission_name)
    permissions.exists?(name: permission_name)
  end
  
  def can_access?(resource)
    case resource
    when Post
      resource.user == self || has_permission?('moderate_posts')
    when User
      self == resource || has_permission?('view_users')
    else
      false
    end
  end
  
  def can_edit?(resource)
    case resource
    when Post
      resource.user == self || has_permission?('edit_posts')
    when User
      self == resource || has_permission?('edit_users')
    else
      false
    end
  end
  
  def can_delete?(resource)
    case resource
    when Post
      resource.user == self || has_permission?('delete_posts')
    when User
      has_permission?('delete_users')
    else
      false
    end
  end
  
  def generate_auth_token
    token = SecureRandom.hex(32)
    sessions.create!(token: token, expires_at: 30.days.from_now)
    token
  end
  
  def invalidate_all_sessions
    sessions.update_all(expires_at: Time.current)
  end
  
  def failed_login_attempt
    attempts = login_attempts.where('created_at >= ?', 1.hour.ago)
    
    if attempts.count >= 5
      update!(status: :suspended)
      AdminMailer.account_suspended(self).deliver_later
    else
      login_attempts.create!(ip_address: Current.request&.remote_ip)
    end
  end
  
  def reset_failed_attempts
    login_attempts.where('created_at >= ?', 1.hour.ago).destroy_all
  end
  
  def full_name
    "#{first_name} #{last_name}".strip
  end
  
  def display_name
    username.presence || full_name.presence || email
  end
  
  def avatar_url
    return profile.avatar.url if profile&.avatar&.present?
    "https://www.gravatar.com/avatar/#{Digest::MD5.hexdigest(email.downcase)}"
  end
  
  private
  
  def set_default_role
    return if roles.any?
    default_role = Role.find_by(name: 'user')
    roles << default_role if default_role
  end
  
  def normalize_email
    self.email = email.downcase.strip if email.present?
  end
  
  def create_profile
    build_profile unless profile.present?
  end
  
  def send_welcome_email
    UserMailer.welcome(self).deliver_later
  end
  
  def log_status_change
    Rails.logger.info "User #{id} status changed from #{status_was} to #{status}"
  end
  
  def password_required?
    new_record? || password.present?
  end
end

Role and Permission Models

class Role < ApplicationRecord
  has_many :user_roles, dependent: :destroy
  has_many :users, through: :user_roles
  has_many :role_permissions, dependent: :destroy
  has_many :permissions, through: :role_permissions
  
  validates :name, presence: true, uniqueness: true
  validates :description, length: { maximum: 500 }
  
  scope :default_roles, -> { where(default: true) }
  scope :active, -> { where(active: true) }
  
  def assign_to_user(user)
    user_roles.create!(user: user) unless user.roles.include?(self)
  end
  
  def remove_from_user(user)
    user_roles.where(user: user).destroy_all
  end
end

class Permission < ApplicationRecord
  has_many :role_permissions, dependent: :destroy
  has_many :roles, through: :role_permissions
  has_many :users, through: :roles
  
  validates :name, presence: true, uniqueness: true
  validates :description, length: { maximum: 500 }
  
  scope :active, -> { where(active: true) }
  scope :by_category, ->(category) { where(category: category) }
  
  def self.seed_default_permissions
    permissions = [
      # User management
      { name: 'view_users', description: 'View user profiles', category: 'user_management' },
      { name: 'edit_users', description: 'Edit user information', category: 'user_management' },
      { name: 'delete_users', description: 'Delete user accounts', category: 'user_management' },
      
      # Content management
      { name: 'create_posts', description: 'Create new posts', category: 'content' },
      { name: 'edit_posts', description: 'Edit any post', category: 'content' },
      { name: 'delete_posts', description: 'Delete any post', category: 'content' },
      { name: 'moderate_posts', description: 'Moderate user posts', category: 'content' },
      
      # System administration
      { name: 'manage_roles', description: 'Manage user roles', category: 'administration' },
      { name: 'view_logs', description: 'View system logs', category: 'administration' },
      { name: 'manage_settings', description: 'Manage system settings', category: 'administration' }
    ]
    
    permissions.each do |permission_data|
      find_or_create_by(name: permission_data[:name]) do |permission|
        permission.assign_attributes(permission_data)
      end
    end
  end
end

Authentication Service

class AuthenticationService
  include ActiveModel::Model
  
  attr_accessor :email, :password, :remember_me, :ip_address
  attr_reader :user, :session
  
  validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :password, presence: true
  
  def initialize(attributes = {})
    super
    @user = nil
    @session = nil
  end
  
  def authenticate
    return false unless valid?
    
    @user = User.find_by(email: email.downcase.strip)
    return false unless user&.authenticate(password)
    
    return false unless user.active?
    
    # Reset failed attempts on successful login
    user.reset_failed_attempts
    
    # Create session
    create_session
    
    # Log successful login
    log_successful_login
    
    true
  rescue => e
    errors.add(:base, "Authentication failed: #{e.message}")
    false
  end
  
  def logout
    return false unless session
    
    session.update!(expires_at: Time.current)
    true
  end
  
  def refresh_token
    return false unless session&.expires_at&.future?
    
    session.update!(expires_at: 30.days.from_now)
    true
  end
  
  private
  
  def create_session
    @session = user.sessions.create!(
      token: SecureRandom.hex(32),
      expires_at: remember_me ? 1.year.from_now : 30.days.from_now,
      ip_address: ip_address,
      user_agent: Current.request&.user_agent
    )
  end
  
  def log_successful_login
    Rails.logger.info "User #{user.id} logged in from #{ip_address}"
    
    # Track login analytics
    AnalyticsService.track('user_login', {
      user_id: user.id,
      ip_address: ip_address,
      user_agent: Current.request&.user_agent
    })
  end
end

# Usage
auth_service = AuthenticationService.new(
  email: '[email protected]',
  password: 'password123',
  remember_me: true,
  ip_address: request.remote_ip
)

if auth_service.authenticate
  session[:user_token] = auth_service.session.token
  redirect_to dashboard_path
else
  flash[:error] = auth_service.errors.full_messages.join(', ')
  render :new
end
Key Benefits: These enhanced examples demonstrate real-world ActiveRecord usage with complex business logic, proper separation of concerns, and scalable architecture patterns.

Overriding Query Methods

How to override ActiveRecord query methods to add custom behavior, default scopes, and query modifications.

Query Overrides: Override ActiveRecord query methods to add default behavior, custom logic, or modify query results. This is useful for adding soft deletes, multi-tenancy, or custom query logic.
Overriding Core Query Methods

Override fundamental query methods to add custom behavior or modify query logic.

Overriding where Method

class User < ApplicationRecord
  # Override where to add default filters
  def self.where(*args)
    # Add default scope for active users
    if args.empty? || !args.first.is_a?(Hash) || !args.first.key?(:active)
      super.where(active: true)
    else
      super
    end
  end

  # Alternative: Override where with custom logic
  def self.where(*args)
    relation = super
    # Add custom filtering logic
    relation = relation.where(tenant_id: Current.tenant_id) if Current.tenant_id
    relation = relation.where(deleted_at: nil) unless args.first&.key?(:include_deleted)
    relation
  end
end

# Usage
User.where(role: 'admin')  # Automatically adds active: true
User.where(active: false)  # Explicitly overrides default
User.where(include_deleted: true)  # Includes deleted records

Overriding find Method

class User < ApplicationRecord
  # Override find to add custom error handling
  def self.find(*args)
    super
  rescue ActiveRecord::RecordNotFound => e
    # Log the error
    Rails.logger.warn("User not found: #{args}")
    # Re-raise with custom message
    raise ActiveRecord::RecordNotFound, "User with ID #{args.first} could not be found"
  end

  # Alternative: Override find with soft delete handling
  def self.find(*args)
    user = super
    if user.deleted_at.present?
      raise ActiveRecord::RecordNotFound, "User has been deleted"
    end
    user
  end
end

# Usage
User.find(999)  # Custom error message
User.find(1)    # Checks if user is soft deleted

Overriding find_by Method

class User < ApplicationRecord
  # Override find_by to add default conditions
  def self.find_by(*args)
    # Add tenant filtering
    conditions = args.first || {}
    conditions[:tenant_id] = Current.tenant_id if Current.tenant_id
    super(conditions)
  end

  # Alternative: Override find_by with caching
  def self.find_by(*args)
    cache_key = "user_find_by_#{args.hash}"
    Rails.cache.fetch(cache_key, expires_in: 1.hour) do
      super
    end
  end
end

# Usage
User.find_by(email: '[email protected]')  # Automatically filters by tenant
User.find_by(email: '[email protected]')  # Uses cached result

Overriding all Method

class User < ApplicationRecord
  # Override all to add default ordering
  def self.all
    super.order(:created_at)
  end

  # Alternative: Override all with tenant filtering
  def self.all
    relation = super
    relation = relation.where(tenant_id: Current.tenant_id) if Current.tenant_id
    relation
  end
end

# Usage
User.all  # Automatically ordered by created_at
User.all  # Automatically filtered by tenant
Soft Delete Implementation

Implement soft delete by overriding query methods to automatically filter out deleted records.

Complete Soft Delete Override

class User < ApplicationRecord
  # Override all query methods to exclude soft deleted records
  def self.where(*args)
    relation = super
    relation = relation.where(deleted_at: nil) unless args.first&.key?(:with_deleted)
    relation
  end

  def self.find(*args)
    user = super
    if user.deleted_at.present?
      raise ActiveRecord::RecordNotFound, "User has been deleted"
    end
    user
  end

  def self.find_by(*args)
    user = super
    if user&.deleted_at.present?
      return nil
    end
    user
  end

  def self.all
    super.where(deleted_at: nil)
  end

  def self.count
    super.where(deleted_at: nil)
  end

  # Custom methods for working with deleted records
  def self.with_deleted
    unscoped.where.not(deleted_at: nil)
  end

  def self.only_deleted
    unscoped.where.not(deleted_at: nil)
  end

  def self.with_deleted_and_active
    unscoped
  end

  # Instance methods
  def soft_delete
    update(deleted_at: Time.current)
  end

  def restore
    update(deleted_at: nil)
  end

  def deleted?
    deleted_at.present?
  end
end

# Usage
User.where(active: true)           # Only active, non-deleted users
User.find(1)                       # Raises error if deleted
User.find_by(email: '[email protected]')  # Returns nil if deleted
User.with_deleted.find(1)          # Includes deleted records
User.only_deleted                  # Only deleted records
user.soft_delete                   # Soft delete a user
user.restore                       # Restore a deleted user

Multi-Tenant Query Override

class User < ApplicationRecord
  # Override query methods to add tenant filtering
  def self.where(*args)
    relation = super
    relation = relation.where(tenant_id: Current.tenant_id) if Current.tenant_id
    relation
  end

  def self.find(*args)
    user = super
    if Current.tenant_id && user.tenant_id != Current.tenant_id
      raise ActiveRecord::RecordNotFound, "User not found in current tenant"
    end
    user
  end

  def self.find_by(*args)
    conditions = args.first || {}
    conditions[:tenant_id] = Current.tenant_id if Current.tenant_id
    super(conditions)
  end

  def self.all
    relation = super
    relation = relation.where(tenant_id: Current.tenant_id) if Current.tenant_id
    relation
  end

  # Custom methods for cross-tenant operations
  def self.global_find(id)
    unscoped.find(id)
  end

  def self.global_where(*args)
    unscoped.where(*args)
  end
end

# Usage
User.where(active: true)           # Automatically filtered by tenant
User.find(1)                       # Checks tenant access
User.global_find(1)                # Bypass tenant filtering
User.global_where(active: true)    # Bypass tenant filtering
Custom Query Builders

Create custom query builders by overriding query methods to add domain-specific logic.

Audit Trail Query Override

class Order < ApplicationRecord
  # Override query methods to add audit trail
  def self.where(*args)
    relation = super
    # Log query for audit purposes
    AuditLog.create!(
      action: 'query',
      table_name: 'orders',
      conditions: args.first,
      user_id: Current.user&.id,
      ip_address: Current.request&.remote_ip
    )
    relation
  end

  def self.find(*args)
    order = super
    # Log record access
    AuditLog.create!(
      action: 'find',
      table_name: 'orders',
      record_id: order.id,
      user_id: Current.user&.id,
      ip_address: Current.request&.remote_ip
    )
    order
  end

  # Custom audit methods
  def self.audited_where(*args)
    relation = where(*args)
    AuditLog.create!(
      action: 'audited_query',
      table_name: 'orders',
      conditions: args.first,
      user_id: Current.user&.id,
      ip_address: Current.request&.remote_ip,
      result_count: relation.count
    )
    relation
  end
end

# Usage
Order.where(status: 'pending')     # Logged automatically
Order.find(1)                      # Logged automatically
Order.audited_where(status: 'pending')  # Explicit audit logging

Caching Query Override

class Product < ApplicationRecord
  # Override query methods to add caching
  def self.where(*args)
    cache_key = "products_where_#{args.hash}"
    Rails.cache.fetch(cache_key, expires_in: 30.minutes) do
      super
    end
  end

  def self.find(*args)
    cache_key = "product_find_#{args.first}"
    Rails.cache.fetch(cache_key, expires_in: 1.hour) do
      super
    end
  end

  def self.find_by(*args)
    cache_key = "product_find_by_#{args.hash}"
    Rails.cache.fetch(cache_key, expires_in: 30.minutes) do
      super
    end
  end

  # Custom cache invalidation
  def self.clear_cache
    Rails.cache.delete_matched("products_*")
  end

  # Override save to invalidate cache
  def save(*args)
    result = super
    if result
      self.class.clear_cache
    end
    result
  end
end

# Usage
Product.where(category: 'electronics')  # Cached for 30 minutes
Product.find(1)                         # Cached for 1 hour
Product.find_by(name: 'iPhone')         # Cached for 30 minutes
Product.clear_cache                     # Clear all product caches

Permission-Based Query Override

class Document < ApplicationRecord
  # Override query methods to add permission filtering
  def self.where(*args)
    relation = super
    # Add permission-based filtering
    if Current.user
      case Current.user.role
      when 'admin'
        # Admins can see all documents
        relation
      when 'manager'
        # Managers can see documents in their departments
        relation = relation.where(department_id: Current.user.department_ids)
      when 'user'
        # Users can only see their own documents
        relation = relation.where(user_id: Current.user.id)
      else
        # No access
        relation = relation.none
      end
    else
      # No user, no access
      relation = relation.none
    end
  end

  def self.find(*args)
    document = super
    # Check if user has permission to access this document
    unless Current.user&.can_access_document?(document)
      raise ActiveRecord::RecordNotFound, "Document not found or access denied"
    end
    document
  end

  def self.find_by(*args)
    document = super
    # Check permission for find_by as well
    if document && !Current.user&.can_access_document?(document)
      return nil
    end
    document
  end

  # Custom methods for bypassing permissions
  def self.admin_find(id)
    unscoped.find(id)
  end

  def self.admin_where(*args)
    unscoped.where(*args)
  end
end

# Usage
Document.where(status: 'published')     # Filtered by permissions
Document.find(1)                        # Permission checked
Document.admin_find(1)                  # Bypass permissions
Document.admin_where(status: 'draft')   # Bypass permissions
Advanced Override Patterns

Advanced patterns for overriding query methods with complex logic and chaining.

Method Chaining Override

class User < ApplicationRecord
  # Override where to support method chaining with custom logic
  def self.where(*args)
    relation = super
    # Add custom behavior that can be chained
    relation = relation.extend(CustomWhereMethods)
    relation
  end

  # Module for custom where methods
  module CustomWhereMethods
    def active_only
      where(active: true)
    end

    def verified_only
      where(verified: true)
    end

    def recent(days = 30)
      where('created_at >= ?', days.days.ago)
    end

    def by_role(role)
      where(role: role)
    end

    def with_posts
      joins(:posts).distinct
    end

    def with_recent_posts
      joins(:posts).where('posts.created_at >= ?', 1.week.ago).distinct
    end
  end
end

# Usage with chaining
User.where(role: 'admin').active_only.verified_only
User.where(active: true).recent(7).with_posts
User.where(verified: true).by_role('user').with_recent_posts

Conditional Override

class Product < ApplicationRecord
  # Override where with conditional logic
  def self.where(*args)
    relation = super
    
    # Add different behavior based on conditions
    if args.first&.key?(:featured)
      relation = relation.where(featured: true)
    end
    
    if args.first&.key?(:on_sale)
      relation = relation.where('discount_percentage > 0')
    end
    
    if args.first&.key?(:in_stock)
      relation = relation.where('stock_quantity > 0')
    end
    
    # Add tenant filtering if in multi-tenant context
    if defined?(Current) && Current.tenant_id
      relation = relation.where(tenant_id: Current.tenant_id)
    end
    
    relation
  end

  # Override find with conditional error handling
  def self.find(*args)
    product = super
    
    # Add custom validation based on context
    if defined?(Current) && Current.user
      unless Current.user.can_view_product?(product)
        raise ActiveRecord::RecordNotFound, "Product not accessible"
      end
    end
    
    product
  rescue ActiveRecord::RecordNotFound => e
    # Log the error for analytics
    if defined?(Rails) && Rails.logger
      Rails.logger.warn("Product not found: #{args} - #{e.message}")
    end
    raise e
  end
end

# Usage
Product.where(featured: true)           # Only featured products
Product.where(on_sale: true)            # Only products with discounts
Product.where(in_stock: true)           # Only products in stock
Product.find(1)                         # With permission checking

Performance Optimization Override

class Post < ApplicationRecord
  # Override query methods to add performance optimizations
  def self.where(*args)
    relation = super
    
    # Automatically include associations for common queries
    if args.first&.key?(:with_comments) || args.first&.key?(:include_comments)
      relation = relation.includes(:comments)
    end
    
    if args.first&.key?(:with_author) || args.first&.key?(:include_author)
      relation = relation.includes(:user)
    end
    
    if args.first&.key?(:with_tags) || args.first&.key?(:include_tags)
      relation = relation.includes(:tags)
    end
    
    # Add default ordering for certain queries
    if args.first&.key?(:recent) || args.first&.key?(:latest)
      relation = relation.order(created_at: :desc)
    end
    
    if args.first&.key?(:popular) || args.first&.key?(:trending)
      relation = relation.order(view_count: :desc)
    end
    
    relation
  end

  def self.find(*args)
    # Automatically include common associations for find
    super.includes(:user, :comments, :tags)
  end

  # Custom methods for specific performance optimizations
  def self.with_full_associations
    includes(:user, :comments, :tags, :category)
  end

  def self.for_dashboard
    includes(:user, :comments).order(created_at: :desc).limit(10)
  end

  def self.for_feed
    includes(:user, :tags).where(published: true).order(published_at: :desc)
  end
end

# Usage
Post.where(with_comments: true)         # Automatically includes comments
Post.where(with_author: true)           # Automatically includes author
Post.where(recent: true)                # Automatically ordered by created_at
Post.find(1)                            # Includes common associations
Post.with_full_associations             # All associations included
Post.for_dashboard                      # Optimized for dashboard
Post.for_feed                           # Optimized for feed
Important: Be careful when overriding query methods. Always call super to maintain the original functionality, and test thoroughly to ensure your overrides don't break existing code.
Best Practices:
  • Always call super in your overrides
  • Test your overrides thoroughly with different scenarios
  • Document the behavior changes clearly
  • Consider performance implications of your overrides
  • Provide methods to bypass overrides when needed

Complete Examples by Case Study

Real-world case studies demonstrating comprehensive ActiveRecord usage in different scenarios.

Case Studies: These examples show how ActiveRecord features work together in real applications, from simple blog systems to complex e-commerce platforms.
Case Study 1: Blog System

A complete blog system with users, posts, comments, and categories.

User Model

class User < ApplicationRecord
  # Associations
  has_many :posts, dependent: :destroy
  has_many :comments, dependent: :destroy
  has_one :profile, dependent: :destroy
  has_many :likes, dependent: :destroy
  has_many :liked_posts, through: :likes, source: :post
  
  # Validations
  validates :username, presence: true, uniqueness: true, 
            length: { minimum: 3, maximum: 30 },
            format: { with: /\A[a-zA-Z0-9_]+\z/ }
  validates :email, presence: true, uniqueness: true, 
            format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :password, presence: true, length: { minimum: 8 }, 
            if: :password_required?
  validates :age, numericality: { greater_than: 13, less_than: 120 }, 
            allow_nil: true
  
  # Scopes
  scope :active, -> { where(active: true) }
  scope :recent, -> { where('created_at > ?', 1.week.ago) }
  scope :admins, -> { where(role: 'admin') }
  scope :moderators, -> { where(role: 'moderator') }
  scope :by_username, ->(username) { where('username ILIKE ?', "%#{username}%") }
  
  # Callbacks
  before_create :set_default_role
  before_save :normalize_email
  after_create :create_profile
  after_save :send_welcome_email, if: :saved_change_to_email?
  
  # Custom methods
  def admin?
    role == 'admin'
  end
  
  def moderator?
    role == 'moderator'
  end
  
  def full_name
    "#{first_name} #{last_name}".strip
  end
  
  def posts_count
    posts.count
  end
  
  def recent_posts(limit = 5)
    posts.order(created_at: :desc).limit(limit)
  end
  
  private
  
  def set_default_role
    self.role ||= 'user'
  end
  
  def normalize_email
    self.email = email.downcase.strip if email.present?
  end
  
  def create_profile
    build_profile unless profile.present?
  end
  
  def send_welcome_email
    UserMailer.welcome(self).deliver_later
  end
  
  def password_required?
    new_record? || password.present?
  end
end

Post Model

class Post < ApplicationRecord
  # Associations
  belongs_to :user
  belongs_to :category, optional: true
  has_many :comments, dependent: :destroy
  has_many :likes, dependent: :destroy
  has_many :liked_by, through: :likes, source: :user
  has_many :taggings, dependent: :destroy
  has_many :tags, through: :taggings
  
  # Validations
  validates :title, presence: true, length: { minimum: 5, maximum: 200 }
  validates :content, presence: true, length: { minimum: 50 }
  validates :status, inclusion: { in: %w[draft published archived] }
  validates :published_at, presence: true, if: :published?
  
  # Scopes
  scope :published, -> { where(status: 'published') }
  scope :draft, -> { where(status: 'draft') }
  scope :recent, -> { where('published_at > ?', 1.month.ago) }
  scope :popular, -> { joins(:likes).group('posts.id').having('COUNT(likes.id) > 5') }
  scope :by_category, ->(category_id) { where(category_id: category_id) }
  scope :search, ->(query) { 
    where('title ILIKE ? OR content ILIKE ?', "%#{query}%", "%#{query}%") 
  }
  
  # Callbacks
  before_save :set_published_at
  after_create :notify_followers
  after_update :update_search_index, if: :saved_change_to_title_or_content?
  
  # Custom methods
  def published?
    status == 'published'
  end
  
  def draft?
    status == 'draft'
  end
  
  def like_count
    likes.count
  end
  
  def comment_count
    comments.count
  end
  
  def excerpt(length = 150)
    content.truncate(length, separator: ' ')
  end
  
  def reading_time
    (content.split.size / 200.0).ceil
  end
  
  private
  
  def set_published_at
    self.published_at = Time.current if published? && published_at.nil?
  end
  
  def notify_followers
    user.followers.each do |follower|
      NotificationMailer.new_post(follower, self).deliver_later
    end
  end
  
  def update_search_index
    SearchIndexJob.perform_later(self)
  end
  
  def saved_change_to_title_or_content?
    saved_change_to_title? || saved_change_to_content?
  end
end

Comment Model

class Comment < ApplicationRecord
  # Associations
  belongs_to :user
  belongs_to :post
  belongs_to :parent, class_name: 'Comment', optional: true
  has_many :replies, class_name: 'Comment', foreign_key: 'parent_id'
  has_many :likes, as: :likeable, dependent: :destroy
  
  # Validations
  validates :content, presence: true, length: { minimum: 2, maximum: 1000 }
  validates :user_id, presence: true
  validates :post_id, presence: true
  
  # Scopes
  scope :approved, -> { where(approved: true) }
  scope :pending, -> { where(approved: false) }
  scope :recent, -> { order(created_at: :desc) }
  scope :top_level, -> { where(parent_id: nil) }
  
  # Callbacks
  before_create :set_default_approval
  after_create :notify_post_author
  after_save :update_post_comment_count
  
  # Custom methods
  def approved?
    approved == true
  end
  
  def has_replies?
    replies.any?
  end
  
  def depth
    return 0 if parent.nil?
    parent.depth + 1
  end
  
  def can_be_replied_to?
    depth < 3  # Limit nesting to 3 levels
  end
  
  private
  
  def set_default_approval
    self.approved = user.admin? || user.moderator?
  end
  
  def notify_post_author
    return if user_id == post.user_id
    NotificationMailer.new_comment(post.user, self).deliver_later
  end
  
  def update_post_comment_count
    post.update_column(:comments_count, post.comments.approved.count)
  end
end

Usage Examples

# Creating a user with profile
user = User.create!(
  username: 'johndoe',
  email: '[email protected]',
  first_name: 'John',
  last_name: 'Doe',
  password: 'securepassword123'
)

# Creating a post
post = user.posts.create!(
  title: 'Getting Started with Rails',
  content: 'Rails is a web application framework...',
  status: 'published',
  category: Category.find_by(name: 'Programming')
)

# Adding comments
comment = post.comments.create!(
  user: User.find_by(username: 'janedoe'),
  content: 'Great article! Very helpful.'
)

# Finding popular posts
popular_posts = Post.published.popular.includes(:user, :category)

# Searching posts
search_results = Post.published.search('rails tutorial')

# User's recent activity
user_activity = user.posts.published.recent.includes(:comments, :likes)
Case Study 2: E-commerce Platform

A comprehensive e-commerce system with products, orders, customers, and inventory management.

Product Model

class Product < ApplicationRecord
  # Associations
  belongs_to :category
  belongs_to :brand, optional: true
  has_many :product_images, dependent: :destroy
  has_many :order_items, dependent: :destroy
  has_many :orders, through: :order_items
  has_many :reviews, dependent: :destroy
  has_many :wishlist_items, dependent: :destroy
  has_many :wishlisted_by, through: :wishlist_items, source: :user
  
  # Validations
  validates :name, presence: true, length: { minimum: 3, maximum: 200 }
  validates :sku, presence: true, uniqueness: true
  validates :price, presence: true, numericality: { greater_than: 0 }
  validates :stock_quantity, numericality: { greater_than_or_equal_to: 0 }
  validates :weight, numericality: { greater_than: 0 }, allow_nil: true
  validates :status, inclusion: { in: %w[active inactive discontinued] }
  
  # Scopes
  scope :active, -> { where(status: 'active') }
  scope :in_stock, -> { where('stock_quantity > 0') }
  scope :out_of_stock, -> { where(stock_quantity: 0) }
  scope :by_price_range, ->(min, max) { where(price: min..max) }
  scope :featured, -> { where(featured: true) }
  scope :on_sale, -> { where('sale_price IS NOT NULL AND sale_price < price') }
  scope :by_category, ->(category_id) { where(category_id: category_id) }
  scope :search, ->(query) { 
    where('name ILIKE ? OR description ILIKE ?', "%#{query}%", "%#{query}%") 
  }
  
  # Callbacks
  before_create :generate_sku
  before_save :calculate_discount_percentage
  after_save :update_category_product_count
  after_update :notify_low_stock, if: :saved_change_to_stock_quantity?
  
  # Custom methods
  def available?
    active? && in_stock?
  end
  
  def in_stock?
    stock_quantity > 0
  end
  
  def current_price
    sale_price || price
  end
  
  def discount_percentage
    return 0 unless sale_price && sale_price < price
    ((price - sale_price) / price * 100).round(2)
  end
  
  def average_rating
    reviews.average(:rating)&.round(2) || 0
  end
  
  def review_count
    reviews.count
  end
  
  def main_image
    product_images.order(:position).first
  end
  
  def update_stock(quantity)
    update(stock_quantity: stock_quantity + quantity)
  end
  
  def reserve_stock(quantity)
    return false if stock_quantity < quantity
    update(stock_quantity: stock_quantity - quantity)
  end
  
  private
  
  def generate_sku
    return if sku.present?
    loop do
      self.sku = "SKU#{SecureRandom.hex(4).upcase}"
      break unless Product.exists?(sku: sku)
    end
  end
  
  def calculate_discount_percentage
    if sale_price.present? && sale_price < price
      self.discount_percentage = ((price - sale_price) / price * 100).round(2)
    else
      self.discount_percentage = 0
    end
  end
  
  def update_category_product_count
    category.update_column(:products_count, category.products.count)
  end
  
  def notify_low_stock
    if stock_quantity <= 10 && stock_quantity_was > 10
      LowStockNotificationJob.perform_later(self)
    end
  end
end

Order Model

class Order < ApplicationRecord
  # Associations
  belongs_to :user
  belongs_to :shipping_address, class_name: 'Address'
  belongs_to :billing_address, class_name: 'Address'
  has_many :order_items, dependent: :destroy
  has_many :products, through: :order_items
  has_many :payments, dependent: :destroy
  has_one :shipping, dependent: :destroy
  
  # Validations
  validates :order_number, presence: true, uniqueness: true
  validates :status, inclusion: { in: %w[pending paid shipped delivered cancelled] }
  validates :subtotal, numericality: { greater_than_or_equal_to: 0 }
  validates :tax_amount, numericality: { greater_than_or_equal_to: 0 }
  validates :shipping_amount, numericality: { greater_than_or_equal_to: 0 }
  validates :total_amount, numericality: { greater_than: 0 }
  
  # Scopes
  scope :pending, -> { where(status: 'pending') }
  scope :paid, -> { where(status: 'paid') }
  scope :shipped, -> { where(status: 'shipped') }
  scope :delivered, -> { where(status: 'delivered') }
  scope :cancelled, -> { where(status: 'cancelled') }
  scope :recent, -> { order(created_at: :desc) }
  scope :by_date_range, ->(start_date, end_date) { 
    where(created_at: start_date.beginning_of_day..end_date.end_of_day) 
  }
  scope :high_value, -> { where('total_amount > ?', 1000) }
  
  # Callbacks
  before_create :generate_order_number
  before_save :calculate_totals
  after_create :reserve_inventory
  after_update :update_inventory, if: :saved_change_to_status?
  
  # Custom methods
  def total_items
    order_items.sum(:quantity)
  end
  
  def can_cancel?
    %w[pending paid].include?(status)
  end
  
  def can_ship?
    status == 'paid'
  end
  
  def can_deliver?
    status == 'shipped'
  end
  
  def is_complete?
    status == 'delivered'
  end
  
  def days_since_ordered
    (Time.current - created_at).to_i / 1.day
  end
  
  def estimated_delivery_date
    created_at + 7.days
  end
  
  def apply_discount(coupon)
    discount_amount = calculate_discount(coupon)
    update(
      discount_amount: discount_amount,
      total_amount: total_amount - discount_amount
    )
  end
  
  def add_product(product, quantity = 1)
    existing_item = order_items.find_by(product: product)
    
    if existing_item
      existing_item.update(quantity: existing_item.quantity + quantity)
    else
      order_items.create!(
        product: product,
        quantity: quantity,
        unit_price: product.current_price
      )
    end
  end
  
  private
  
  def generate_order_number
    return if order_number.present?
    loop do
      self.order_number = "ORD#{Time.current.strftime('%Y%m%d')}#{SecureRandom.hex(3).upcase}"
      break unless Order.exists?(order_number: order_number)
    end
  end
  
  def calculate_totals
    self.subtotal = order_items.sum('quantity * unit_price')
    self.tax_amount = subtotal * 0.08  # 8% tax
    self.total_amount = subtotal + tax_amount + shipping_amount - discount_amount.to_f
  end
  
  def reserve_inventory
    order_items.each do |item|
      item.product.reserve_stock(item.quantity)
    end
  end
  
  def update_inventory
    case status
    when 'cancelled'
      order_items.each do |item|
        item.product.update_stock(item.quantity)
      end
    when 'shipped'
      create_shipping(tracking_number: generate_tracking_number)
    end
  end
  
  def calculate_discount(coupon)
    return 0 unless coupon.valid_for_order?(self)
    (subtotal * coupon.discount_percentage / 100.0).round(2)
  end
  
  def generate_tracking_number
    "TRK#{SecureRandom.hex(6).upcase}"
  end
end

Usage Examples

# Creating a product
product = Product.create!(
  name: 'Wireless Headphones',
  description: 'High-quality wireless headphones with noise cancellation',
  price: 299.99,
  sale_price: 249.99,
  stock_quantity: 50,
  category: Category.find_by(name: 'Electronics'),
  brand: Brand.find_by(name: 'TechCorp')
)

# Creating an order
order = user.orders.create!(
  shipping_address: user.addresses.first,
  billing_address: user.addresses.first,
  status: 'pending'
)

# Adding products to order
order.add_product(Product.find_by(name: 'Wireless Headphones'), 2)
order.add_product(Product.find_by(name: 'Phone Case'), 1)

# Finding products
featured_products = Product.active.featured.includes(:category, :brand)
on_sale_products = Product.active.on_sale.by_price_range(50, 200)
search_results = Product.active.search('wireless headphones')

# Order management
pending_orders = Order.pending.includes(:user, :order_items)
recent_orders = Order.recent.limit(10)
high_value_orders = Order.high_value.includes(:user)
Case Study 3: Social Media Platform

A social media platform with users, posts, relationships, and engagement features.

User Model (Social)

class User < ApplicationRecord
  # Associations
  has_many :posts, dependent: :destroy
  has_many :comments, dependent: :destroy
  has_many :likes, dependent: :destroy
  has_many :follows_as_follower, class_name: 'Follow', foreign_key: 'follower_id'
  has_many :follows_as_following, class_name: 'Follow', foreign_key: 'following_id'
  has_many :followers, through: :follows_as_following, source: :follower
  has_many :following, through: :follows_as_follower, source: :following
  has_many :notifications, dependent: :destroy
  has_one :profile, dependent: :destroy
  has_many :messages_sent, class_name: 'Message', foreign_key: 'sender_id'
  has_many :messages_received, class_name: 'Message', foreign_key: 'recipient_id'
  
  # Validations
  validates :username, presence: true, uniqueness: true,
            format: { with: /\A[a-zA-Z0-9_]+\z/ },
            length: { minimum: 3, maximum: 30 }
  validates :email, presence: true, uniqueness: true,
            format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :password, presence: true, length: { minimum: 8 },
            if: :password_required?
  validates :bio, length: { maximum: 500 }
  
  # Scopes
  scope :active, -> { where(active: true) }
  scope :verified, -> { where(verified: true) }
  scope :recent, -> { where('created_at > ?', 1.week.ago) }
  scope :by_username, ->(username) { where('username ILIKE ?', "%#{username}%") }
  scope :popular, -> { 
    joins(:followers).group('users.id').having('COUNT(followers.id) > 100') 
  }
  
  # Callbacks
  before_create :set_default_privacy
  before_save :normalize_username
  after_create :create_profile
  after_save :update_search_index, if: :saved_change_to_username_or_bio?
  
  # Custom methods
  def follow(user)
    return if self == user || following?(user)
    follows_as_follower.create(following: user)
  end
  
  def unfollow(user)
    follows_as_follower.find_by(following: user)&.destroy
  end
  
  def following?(user)
    following.include?(user)
  end
  
  def followers_count
    followers.count
  end
  
  def following_count
    following.count
  end
  
  def posts_count
    posts.count
  end
  
  def feed
    Post.where(user: [self] + following).includes(:user, :likes, :comments)
        .order(created_at: :desc)
  end
  
  def recent_activity
    posts.includes(:likes, :comments).order(created_at: :desc).limit(10)
  end
  
  def mutual_followers_with(user)
    followers & user.followers
  end
  
  def suggested_users
    User.where.not(id: [self] + following.pluck(:id))
        .joins(:followers)
        .group('users.id')
        .order('COUNT(followers.id) DESC')
        .limit(5)
  end
  
  private
  
  def set_default_privacy
    self.privacy_level ||= 'public'
  end
  
  def normalize_username
    self.username = username.downcase if username.present?
  end
  
  def create_profile
    build_profile unless profile.present?
  end
  
  def update_search_index
    SearchIndexJob.perform_later(self)
  end
  
  def saved_change_to_username_or_bio?
    saved_change_to_username? || saved_change_to_bio?
  end
end

Post Model (Social)

class Post < ApplicationRecord
  # Associations
  belongs_to :user
  has_many :comments, dependent: :destroy
  has_many :likes, as: :likeable, dependent: :destroy
  has_many :liked_by, through: :likes, source: :user
  has_many :shares, dependent: :destroy
  has_many :shared_by, through: :shares, source: :user
  has_many :hashtags, through: :post_hashtags
  has_many :post_hashtags, dependent: :destroy
  has_many :media_attachments, dependent: :destroy
  
  # Validations
  validates :content, presence: true, length: { maximum: 1000 }
  validates :visibility, inclusion: { in: %w[public friends private] }
  validates :user_id, presence: true
  
  # Scopes
  scope :public_posts, -> { where(visibility: 'public') }
  scope :recent, -> { order(created_at: :desc) }
  scope :popular, -> { 
    joins(:likes).group('posts.id').having('COUNT(likes.id) > 10') 
  }
  scope :trending, -> { 
    joins(:likes, :comments, :shares)
    .group('posts.id')
    .order('(COUNT(likes.id) + COUNT(comments.id) * 2 + COUNT(shares.id) * 3) DESC')
  }
  scope :by_hashtag, ->(hashtag) { 
    joins(:hashtags).where(hashtags: { name: hashtag }) 
  }
  
  # Callbacks
  before_save :extract_hashtags
  after_create :notify_followers
  after_update :update_trending_score
  
  # Custom methods
  def like_count
    likes.count
  end
  
  def comment_count
    comments.count
  end
  
  def share_count
    shares.count
  end
  
  def engagement_score
    like_count + (comment_count * 2) + (share_count * 3)
  end
  
  def can_be_viewed_by?(viewer)
    return true if user == viewer
    return true if visibility == 'public'
    return true if visibility == 'friends' && user.following?(viewer)
    false
  end
  
  def can_be_liked_by?(user)
    user.present? && can_be_viewed_by?(user)
  end
  
  def can_be_commented_by?(user)
    user.present? && can_be_viewed_by?(user)
  end
  
  def hashtag_names
    hashtags.pluck(:name)
  end
  
  def add_hashtag(hashtag_name)
    hashtag = Hashtag.find_or_create_by(name: hashtag_name.downcase)
    hashtags << hashtag unless hashtags.include?(hashtag)
  end
  
  def remove_hashtag(hashtag_name)
    hashtag = hashtags.find_by(name: hashtag_name.downcase)
    hashtags.delete(hashtag) if hashtag
  end
  
  private
  
  def extract_hashtags
    hashtag_pattern = /#(\w+)/
    content.scan(hashtag_pattern).flatten.each do |hashtag_name|
      add_hashtag(hashtag_name)
    end
  end
  
  def notify_followers
    user.followers.each do |follower|
      Notification.create!(
        user: follower,
        notifiable: self,
        action: 'new_post',
        message: "#{user.username} posted something new"
      )
    end
  end
  
  def update_trending_score
    update_column(:trending_score, engagement_score)
  end
end

Follow Model

class Follow < ApplicationRecord
  # Associations
  belongs_to :follower, class_name: 'User'
  belongs_to :following, class_name: 'User'
  
  # Validations
  validates :follower_id, presence: true
  validates :following_id, presence: true
  validates :follower_id, uniqueness: { scope: :following_id }
  validate :cannot_follow_self
  
  # Callbacks
  after_create :notify_following
  after_destroy :remove_notification
  
  # Custom methods
  def mutual?
    following.following?(follower)
  end
  
  private
  
  def cannot_follow_self
    if follower_id == following_id
      errors.add(:base, "Cannot follow yourself")
    end
  end
  
  def notify_following
    Notification.create!(
      user: following,
      notifiable: follower,
      action: 'new_follower',
      message: "#{follower.username} started following you"
    )
  end
  
  def remove_notification
    Notification.where(
      user: following,
      notifiable: follower,
      action: 'new_follower'
    ).destroy_all
  end
end

Usage Examples

# Creating a user
user = User.create!(
  username: 'johndoe',
  email: '[email protected]',
  bio: 'Software developer and coffee enthusiast',
  password: 'securepassword123'
)

# Following another user
user.follow(User.find_by(username: 'janedoe'))

# Creating a post
post = user.posts.create!(
  content: 'Just finished building my first Rails app! #rails #programming',
  visibility: 'public'
)

# Liking a post
post.likes.create!(user: User.find_by(username: 'janedoe'))

# Getting user feed
user_feed = user.feed.includes(:user, :likes, :comments)

# Finding trending posts
trending_posts = Post.public_posts.trending.limit(10)

# Finding posts by hashtag
rails_posts = Post.public_posts.by_hashtag('rails')

# User suggestions
suggestions = user.suggested_users
Key Takeaways: These case studies demonstrate how ActiveRecord features work together in real applications. Notice the use of associations, validations, scopes, callbacks, and custom methods to create robust, maintainable models.

Rails Commands Reference

Complete reference of Rails and ActiveRecord terminal commands for development, database management, and deployment.

Terminal Commands: This section contains all the essential Rails commands you'll need for ActiveRecord development, from generating models to managing databases.
Model Generation

Generate Model

Description: Creates a new model with its migration file.

# Basic model generation
rails generate model User name:string email:string

# Model with different data types
rails generate model Post title:string content:text published:boolean

# Model with references (foreign keys)
rails generate model Comment content:text user:references post:references

# Model with timestamps (default)
rails generate model Product name:string price:decimal

# Model without timestamps
rails generate model Setting key:string value:text --no-timestamps

# Model with custom table name
rails generate model UserProfile user:references bio:text --table-name=profiles

Generate Migration

Description: Creates a new migration file for database changes.

# Add column to existing table
rails generate migration AddEmailToUsers email:string

# Remove column from table
rails generate migration RemoveEmailFromUsers email:string

# Add index to table
rails generate migration AddIndexToUsers email:string:index

# Add reference to table
rails generate migration AddUserToPosts user:references

# Create join table
rails generate migration CreateJoinTableUsersPosts users posts

# Custom migration
rails generate migration CreateUsers

Generate Controller

Description: Creates a new controller with actions.

# Basic controller
rails generate controller Users index show create update destroy

# Controller with namespace
rails generate controller Admin::Users index show

# API controller
rails generate controller Api::V1::Users index show --skip-template-engine

# Controller with custom actions
rails generate controller Posts index show new create edit update destroy
Database Management

Database Creation and Setup

Description: Commands for creating and setting up databases.

# Create database
rails db:create

# Create database for specific environment
rails db:create RAILS_ENV=production

# Drop database
rails db:drop

# Reset database (drop, create, migrate, seed)
rails db:reset

# Setup database (create, migrate, seed)
rails db:setup

# Prepare database for testing
rails db:test:prepare

Migration Commands

Description: Commands for running and managing migrations.

# Run pending migrations
rails db:migrate

# Run migrations for specific environment
rails db:migrate RAILS_ENV=production

# Rollback last migration
rails db:rollback

# Rollback multiple migrations
rails db:rollback STEP=3

# Rollback to specific version
rails db:migrate VERSION=20230101000000

# Show migration status
rails db:migrate:status

# Show SQL for migration
rails db:migrate:status

Seed and Fixtures

Description: Commands for populating database with test data.

# Run seed file
rails db:seed

# Run seed for specific environment
rails db:seed RAILS_ENV=production

# Reset and seed
rails db:reset

# Load fixtures
rails db:fixtures:load

# Load specific fixture
rails db:fixtures:load FIXTURES=users

# Export fixtures from database
rails db:fixtures:dump
Console and Debugging

Rails Console

Description: Commands for accessing Rails console and debugging.

# Start Rails console
rails console
# or
rails c

# Start console for specific environment
rails console production
rails c production

# Start console with sandbox (rollback changes)
rails console --sandbox
rails c --sandbox

# Start console with custom environment
rails console RAILS_ENV=staging

Debugging Commands

Description: Commands for debugging and development.

# Start Rails server
rails server
# or
rails s

# Start server on specific port
rails server -p 3001

# Start server in background
rails server -d

# Start server for specific environment
rails server -e production

# Check routes
rails routes

# Check routes for specific controller
rails routes | grep Users

# Check routes in grep format
rails routes -g users
Testing Commands

Test Generation

Description: Commands for generating and running tests.

# Generate model test
rails generate model User name:string --test-framework=rspec

# Generate controller test
rails generate controller Users index show --test-framework=rspec

# Generate integration test
rails generate integration_test UserFlows

# Generate system test
rails generate system_test Users

Running Tests

Description: Commands for running different types of tests.

# Run all tests
rails test

# Run specific test file
rails test test/models/user_test.rb

# Run specific test method
rails test test/models/user_test.rb:25

# Run tests with verbose output
rails test -v

# Run tests with coverage
COVERAGE=true rails test

# Run system tests
rails test:system

# Run integration tests
rails test:integration
Asset and Environment

Asset Pipeline

Description: Commands for managing assets and compilation.

# Precompile assets
rails assets:precompile

# Clean precompiled assets
rails assets:clean

# Clobber all assets
rails assets:clobber

# Check asset paths
rails assets:environment

# Show asset paths
rails assets:paths

Environment and Configuration

Description: Commands for environment management and configuration.

# Show Rails environment
rails about

# Show Rails version
rails version

# Show middleware stack
rails middleware

# Show initializers
rails initializers

# Show routes in different formats
rails routes -c
rails routes -e
rails routes -g
rails routes -x
Deployment and Production

Production Setup

Description: Commands for production deployment and maintenance.

# Precompile assets for production
RAILS_ENV=production rails assets:precompile

# Run migrations in production
RAILS_ENV=production rails db:migrate

# Seed production database
RAILS_ENV=production rails db:seed

# Check production logs
tail -f log/production.log

# Start production server
RAILS_ENV=production rails server

# Check production environment
RAILS_ENV=production rails about

Maintenance and Monitoring

Description: Commands for application maintenance and monitoring.

# Check application status
rails runner "puts Rails.application.config.relative_url_root"

# Check database connection
rails runner "ActiveRecord::Base.connection.execute('SELECT 1')"

# Check pending migrations
rails runner "puts ActiveRecord::Migrator.needs_migration?"

# Check environment variables
rails runner "puts ENV['DATABASE_URL']"

# Monitor application performance
rails runner "puts ActiveRecord::Base.connection.execute('SHOW STATS')"
Pro Tip: Use rails -T to see all available Rails commands, and rails generate -h to see all available generators.

Additional Reference Materials

Comprehensive reference materials and cheat sheets for quick lookup.

Quick Reference: Use these materials for quick lookup and reference during development.
ActiveRecord Cheat Sheet

Model Definition

class User < ApplicationRecord
  # Associations
  has_many :posts, dependent: :destroy
  belongs_to :company
  has_one :profile
  
  # Validations
  validates :email, presence: true, uniqueness: true
  validates :age, numericality: { greater_than: 18 }
  
  # Scopes
  scope :active, -> { where(active: true) }
  scope :recent, -> { where('created_at > ?', 1.week.ago) }
  
  # Enums
  enum status: { pending: 0, active: 1, banned: 2 }
  
  # Callbacks
  before_create :set_default_role
  after_save :send_notification
end

Query Methods

MethodExampleReturns
findUser.find(1)Record
find_byUser.find_by(email: '[email protected]')Record or nil
whereUser.where(active: true)Relation
orderUser.order(:name)Relation
limitUser.limit(10)Relation
countUser.countInteger
sumOrder.sum(:total)Number
averageUser.average(:age)Number
joinsUser.joins(:posts)Relation
includesUser.includes(:posts)Relation

Association Types

# One-to-Many
has_many :posts
belongs_to :user

# One-to-One
has_one :profile
belongs_to :user

# Many-to-Many
has_many :categories, through: :product_categories
has_many :product_categories, dependent: :destroy

# Polymorphic
has_many :comments, as: :commentable
belongs_to :commentable, polymorphic: true

Validation Types

# Presence
validates :email, presence: true

# Uniqueness
validates :email, uniqueness: true

# Length
validates :name, length: { minimum: 2, maximum: 50 }

# Format
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }

# Numericality
validates :age, numericality: { greater_than: 18 }

# Custom
validate :custom_validation_method
Common Patterns

Authentication Patterns

# User authentication
class User < ApplicationRecord
  has_secure_password
  
  def self.authenticate(email, password)
    user = find_by(email: email.downcase)
    user&.authenticate(password)
  end
end

# Usage
user = User.authenticate('[email protected]', 'password123')

Search Patterns

# Basic search
def self.search(query)
  where('name ILIKE ? OR email ILIKE ?', "%#{query}%", "%#{query}%")
end

# Advanced search with filters
def self.search_with_filters(filters = {})
  relation = all
  relation = relation.where(role: filters[:role]) if filters[:role]
  relation = relation.where('created_at >= ?', filters[:since]) if filters[:since]
  relation
end

Pagination Patterns

# Simple pagination
def self.paginate(page = 1, per_page = 20)
  offset((page - 1) * per_page).limit(per_page)
end

# Usage
users = User.paginate(params[:page], 10)

Soft Delete Patterns

# Soft delete implementation
class User < ApplicationRecord
  scope :active, -> { where(deleted_at: nil) }
  
  def soft_delete
    update(deleted_at: Time.current)
  end
  
  def restore
    update(deleted_at: nil)
  end
end

Quick Reference

  • Model: class User < ApplicationRecord
  • Association: has_many :posts
  • Validation: validates :email, presence: true
  • Query: User.where(active: true)
  • Scope: scope :active, -> { where(active: true) }
Best Practices:
  • Keep models thin and controllers skinny
  • Use strong parameters in controllers
  • Validate data at the model level
  • Use database indexes for performance
  • Write tests for your models

Learn more about Rails
Learn more about DevOps

Scroll to Top