ActiveRecord Documentation
Complete guide to Rails ORM
Overview
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.
- 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
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]"
)
ActiveRecord follows Rails conventions, which means less configuration and more productivity:
Database | Ruby/Rails | Example |
---|---|---|
Table name | Model class name (singular) | users table → User class |
Primary key | Always named 'id' | id column |
Foreign keys | End with '_id' | user_id , post_id |
Timestamps | Automatic created_at/updated_at | created_at , updated_at |
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
- 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
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.
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 IDUser.all
- Get all recordsUser.create(attributes)
- Create and save a recordUser.new(attributes)
- Create without savinguser.save
- Save a record to databaseuser.destroy
- Delete a record
ActiveRecord follows strict naming conventions that make your code more predictable and require less configuration:
Database | Model Class | Example |
---|---|---|
Table name (plural, snake_case) | Model name (singular, PascalCase) | users → User |
Table name (plural, snake_case) | Model name (singular, PascalCase) | blog_posts → BlogPost |
Primary key column | Always named 'id' | id (auto-incrementing) |
Foreign key columns | End with '_id' | user_id , post_id |
Timestamps | Automatic columns | created_at , updated_at |
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 keyUser.find_by(attribute: value)
- Find first matchUser.where(conditions)
- Find all matchesUser.first
- Get first recordUser.last
- Get last recordUser.count
- Count all records
Persistence Methods
user.save
- Save to databaseuser.save!
- Save or raise erroruser.update(attributes)
- Update and saveuser.update!(attributes)
- Update or raise erroruser.destroy
- Delete recorduser.delete
- Delete without callbacks
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
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
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.
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
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
Type | Description | Example |
---|---|---|
string | Short text (VARCHAR) | t.string :name |
text | Long text (TEXT) | t.text :bio |
integer | Whole numbers | t.integer :age |
decimal | Precise decimal numbers | t.decimal :price, precision: 10, scale: 2 |
boolean | True/false values | t.boolean :active |
date | Date only | t.date :birth_date |
datetime | Date and time | t.datetime :last_login_at |
timestamps | created_at and updated_at | t.timestamps |
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
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
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
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.
ActiveRecord provides four main types of associations, each representing a different relationship pattern:
Association Type | Relationship | Foreign Key | Example |
---|---|---|---|
belongs_to | One-to-One (child) | In this model | belongs_to :author |
has_one | One-to-One (parent) | In other model | has_one :profile |
has_many | One-to-Many | In other model | has_many :posts |
has_many :through | Many-to-Many | In join table | has_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
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
Option | Description | Example |
---|---|---|
dependent | What happens when parent is deleted | dependent: :destroy |
class_name | Specify the model class name | class_name: 'Post' |
foreign_key | Specify the foreign key column | foreign_key: 'author_id' |
optional | Allow null foreign keys | optional: true |
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 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
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
Option | Description | Example |
---|---|---|
dependent | What happens when parent is deleted | dependent: :destroy |
class_name | Specify the model class name | class_name: 'Post' |
foreign_key | Specify the foreign key column | foreign_key: 'author_id' |
primary_key | Specify the primary key column | primary_key: 'uuid' |
optional | Allow null foreign keys | optional: true |
counter_cache | Cache the count of associated records | counter_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 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
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
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.
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/falsesave!
- Validations run, raises exception on failurecreate
- Validations run, returns objectcreate!
- Validations run, raises exception on failureupdate
- Validations run, returns true/falseupdate!
- 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 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 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
Option | Description | Example |
---|---|---|
case_sensitive | Case-sensitive uniqueness check | case_sensitive: false |
scope | Uniqueness within a scope | scope: :user_id |
allow_blank | Allow blank values to be unique | allow_blank: true |
allow_nil | Allow nil values to be unique | allow_nil: true |
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
Option | Description | Example |
---|---|---|
minimum | Minimum length required | minimum: 2 |
maximum | Maximum length allowed | maximum: 50 |
in | Length within a range | in: 3..20 |
is | Exact length required | is: 8 |
allow_blank | Skip validation if blank | allow_blank: true |
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 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
Option | Description | Example |
---|---|---|
only_integer | Must be an integer | only_integer: true |
greater_than | Must be greater than value | greater_than: 0 |
greater_than_or_equal_to | Must be greater than or equal to value | greater_than_or_equal_to: 0 |
less_than | Must be less than value | less_than: 100 |
less_than_or_equal_to | Must be less than or equal to value | less_than_or_equal_to: 100 |
equal_to | Must be equal to value | equal_to: 42 |
other_than | Must be different from value | other_than: 0 |
in | Must be in range | in: 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 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
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
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.
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
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 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
- 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.
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 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"
- 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.
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
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"]
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
- 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.
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 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 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 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 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
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
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?
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.
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 Type | When to Use | Example | Returns |
---|---|---|---|
Finding Records | Get specific records by ID or attributes | User.find(1) | Single record or array |
Filtering | Get records matching conditions | User.where(active: true) | Relation (chainable) |
Sorting | Order results by specific columns | User.order(:name) | Relation (chainable) |
Limiting | Restrict number of results | User.limit(10) | Relation (chainable) |
Aggregating | Calculate sums, averages, counts | User.count | Number |
Joining | Combine data from multiple tables | User.joins(:posts) | Relation (chainable) |
Eager Loading | Prevent N+1 query problems | User.includes(:posts) | Relation (chainable) |
Grouping | Group records for analysis | User.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
# ❌ 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)
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
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
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)
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)
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 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)
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 oflength
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 offind_each
for large datasets - Not using indexes on frequently queried columns
- Using
length
instead ofcount
for large datasets
Complete reference of all ActiveRecord query methods with descriptions, examples, and usage scenarios.
Query Methods Reference Table
Method | Description | Syntax | Example | Returns |
---|---|---|---|---|
find | Find by primary key, raises error if not found | find(id) | User.find(1) | Record or raises error |
find_by | Find by attributes, returns nil if not found | find_by(attributes) | User.find_by(email: '[email protected]') | Record or nil |
find_by! | Same as find_by but raises error | find_by!(attributes) | User.find_by!(email: '[email protected]') | Record or raises error |
first | Get first record | first | User.first | Record or nil |
last | Get last record | last | User.last | Record or nil |
take | Get first n records without ordering | take(limit) | User.take(5) | Array of records |
where | Filter records by conditions | where(conditions) | User.where(active: true) | Relation |
where.not | Exclude records by conditions | where.not(conditions) | User.where.not(role: 'admin') | Relation |
where.or | OR logic for conditions | where.or(conditions) | User.where(role: 'admin').or(User.where(role: 'moderator')) | Relation |
order | Sort results | order(attributes) | User.order(:name) | Relation |
limit | Limit number of results | limit(number) | User.limit(10) | Relation |
offset | Skip records (pagination) | offset(number) | User.offset(10) | Relation |
distinct | Remove duplicate records | distinct | User.distinct | Relation |
select | Choose specific columns | select(columns) | User.select(:id, :name, :email) | Relation |
pluck | Get array of values | pluck(columns) | User.pluck(:name) | Array |
ids | Get array of primary keys | ids | User.ids | Array |
count | Count records | count | User.count | Integer |
sum | Sum values in column | sum(column) | Order.sum(:total_amount) | Number |
average | Calculate average | average(column) | User.average(:age) | Number |
maximum | Find maximum value | maximum(column) | User.maximum(:age) | Value |
minimum | Find minimum value | minimum(column) | User.minimum(:age) | Value |
joins | INNER JOIN with association | joins(association) | User.joins(:posts) | Relation |
left_joins | LEFT OUTER JOIN | left_joins(association) | User.left_joins(:posts) | Relation |
includes | Eager loading (prevents N+1) | includes(association) | User.includes(:posts) | Relation |
preload | Alternative eager loading | preload(association) | User.preload(:posts) | Relation |
exists? | Check if records exist | exists? | User.exists? | Boolean |
any? | Check if relation has records | any? | User.any? | Boolean |
none? | Check if relation is empty | none? | User.none? | Boolean |
group | Group records by column | group(column) | User.group(:role) | Relation |
having | Filter grouped results | having(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')
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.
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 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 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 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
Enhanced Real-World Examples
Advanced examples demonstrating ActiveRecord in complex real-world scenarios with practical business logic.
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
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
Overriding Query Methods
How to override ActiveRecord query methods to add custom behavior, default scopes, and query modifications.
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
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
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 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
super
to maintain the original functionality, and test thoroughly to ensure your overrides don't break existing code.- 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.
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)
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)
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
Rails Commands Reference
Complete reference of Rails and ActiveRecord terminal commands for development, database management, and deployment.
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 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
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
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 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
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')"
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.
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
Method | Example | Returns |
---|---|---|
find | User.find(1) | Record |
find_by | User.find_by(email: '[email protected]') | Record or nil |
where | User.where(active: true) | Relation |
order | User.order(:name) | Relation |
limit | User.limit(10) | Relation |
count | User.count | Integer |
sum | Order.sum(:total) | Number |
average | User.average(:age) | Number |
joins | User.joins(:posts) | Relation |
includes | User.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
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) }
- 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
https://shorturl.fm/Qj63E
https://shorturl.fm/yiHJN
https://shorturl.fm/xtyxw
https://shorturl.fm/gPF18
https://shorturl.fm/sVeLF
https://shorturl.fm/VucUT
https://shorturl.fm/zpEDO
https://shorturl.fm/BpOxl
https://shorturl.fm/8MWea
https://shorturl.fm/su6WD
https://shorturl.fm/pxApx
https://shorturl.fm/bG3f8
https://shorturl.fm/JbfUT
https://shorturl.fm/1QXtp
https://shorturl.fm/EWEGy
https://shorturl.fm/OB8Cq
https://shorturl.fm/ACGM9