Introduction to ActiveRecord
ActiveRecord is the Object-Relational Mapping (ORM) layer in Ruby on Rails that provides the interface between your Ruby code and your database. It allows you to interact with database records as Ruby objects, eliminating the need for writing raw SQL in most cases. ActiveRecord implements the Active Record pattern, which connects objects to database tables, providing a clean and intuitive API for database operations.
Core Concepts
Model Basics
Concept | Description |
---|---|
Model Definition | class User < ApplicationRecord |
Table Naming | Plural, snake_case (e.g., users table for User model) |
Primary Key | Defaults to id (integer or UUID) |
Timestamps | created_at and updated_at are added automatically |
Creating Models
# Generate a model with rails generator
rails generate model User name:string email:string age:integer
# Migration file created
class CreateUsers < ActiveRecord::Migration[7.0]
def change
create_table :users do |t|
t.string :name
t.string :email
t.integer :age
t.timestamps
end
end
end
# Run migrations
rails db:migrate
CRUD Operations
Create
# Method 1: new + save
user = User.new(name: "John", email: "john@example.com")
user.save
# Method 2: create
user = User.create(name: "John", email: "john@example.com")
# Method 3: create!
user = User.create!(name: "John", email: "john@example.com") # Raises exception on failure
# Bulk insert
User.insert_all([
{ name: "User 1", email: "user1@example.com" },
{ name: "User 2", email: "user2@example.com" }
])
Read
# Find by id
user = User.find(1)
# Find by attributes
user = User.find_by(email: "john@example.com")
user = User.find_by!(email: "john@example.com") # Raises exception if not found
# Find all matching
users = User.where(age: 30)
users = User.where("age > ?", 30)
users = User.where(age: 25..30)
# Get all records
users = User.all
# First and last
user = User.first
user = User.last
# Custom ordering
users = User.order(created_at: :desc)
users = User.order(:name)
Update
# Method 1: find + save
user = User.find(1)
user.name = "New Name"
user.save
# Method 2: update
user = User.find(1)
user.update(name: "New Name")
# Method 3: update!
user.update!(name: "New Name") # Raises exception on failure
# Bulk update
User.where(age: 30).update_all(status: "active")
# Update counters
User.increment_counter(:login_count, user.id)
Delete
# Method 1: destroy (runs callbacks)
user = User.find(1)
user.destroy
# Method 2: destroy! (raises exception on failure)
user.destroy!
# Method 3: delete (skips callbacks)
User.delete(1)
# Bulk delete
User.where(inactive: true).destroy_all
User.destroy_by(inactive: true)
# Delete all records
User.delete_all # No callbacks, faster
User.destroy_all # With callbacks
Querying
Basic Queries
# Simple conditions
User.where(active: true)
User.where.not(active: true)
User.where(age: 20..30)
# SQL fragments
User.where("age > ?", 21)
User.where("name LIKE ?", "%Smith%")
# OR conditions
User.where(active: true).or(User.where("age > ?", 25))
# AND conditions
User.where(active: true).where("age > ?", 25)
# Order
User.order(created_at: :desc)
User.order(name: :asc, created_at: :desc)
# Limit & offset
User.limit(10)
User.offset(10).limit(10) # For pagination
Scopes
# Define scope in model
class User < ApplicationRecord
scope :active, -> { where(active: true) }
scope :recent, -> { order(created_at: :desc) }
scope :adult, ->(min_age = 18) { where("age >= ?", min_age) }
# Alternative with class method
def self.premium
where(premium: true)
end
end
# Using scopes
User.active
User.recent.limit(5)
User.adult(21)
User.active.adult.recent
Joins and Includes
# Joins (INNER JOIN)
User.joins(:posts)
User.joins(:posts, :comments)
User.joins(posts: :comments)
# Left Outer Join
User.left_outer_joins(:posts)
# Includes (eager loading)
User.includes(:posts)
User.includes(:posts, :comments)
User.includes(posts: :comments)
# Complex joins with conditions
User.joins(:posts).where(posts: { published: true })
# Select specific columns
User.select(:id, :name, :email)
User.select("users.*, COUNT(posts.id) as posts_count").joins(:posts).group("users.id")
Aggregations
# Count
User.count
User.where(active: true).count
# Sum, Average, Min, Max
Order.sum(:total)
Order.average(:total)
Order.minimum(:total)
Order.maximum(:total)
# Grouping
Order.group(:status).count
Order.group(:status).sum(:total)
Order.group("DATE(created_at)").count
# Having (SQL HAVING)
Order.group(:user_id).having("sum(total) > ?", 100)
Associations
Types of Associations
# One-to-one
class User < ApplicationRecord
has_one :profile
end
class Profile < ApplicationRecord
belongs_to :user
end
# One-to-many
class User < ApplicationRecord
has_many :posts
end
class Post < ApplicationRecord
belongs_to :user
end
# Many-to-many
class User < ApplicationRecord
has_many :enrollments
has_many :courses, through: :enrollments
end
class Course < ApplicationRecord
has_many :enrollments
has_many :users, through: :enrollments
end
class Enrollment < ApplicationRecord
belongs_to :user
belongs_to :course
end
# Polymorphic associations
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end
class Post < ApplicationRecord
has_many :comments, as: :commentable
end
class Photo < ApplicationRecord
has_many :comments, as: :commentable
end
Association Options
# Common options
has_many :posts, dependent: :destroy
has_many :comments, dependent: :nullify
has_many :recent_posts, -> { where('created_at > ?', 1.week.ago) }, class_name: 'Post'
belongs_to :author, class_name: 'User', foreign_key: 'user_id'
has_many :posts, inverse_of: :user
has_many :tags, through: :taggings, source: :tag
Working with Associations
# Creating associated records
user.posts.create(title: "New Post")
# Building (without saving)
post = user.posts.build(title: "New Post")
# Finding associated records
user.posts
user.posts.find(1)
user.posts.where(published: true)
# Checking associations
user.posts.empty?
user.posts.exists?(title: "Example")
# Counting
user.posts.count
# Adding to collections
user.posts << Post.new(title: "Another Post")
# Removing from collections
user.posts.delete(post)
user.posts.destroy(post)
user.posts.clear
Validations
Common Validations
class User < ApplicationRecord
# Presence validations
validates :name, :email, presence: true
# Length validations
validates :password, length: { minimum: 8, maximum: 20 }
validates :bio, length: { maximum: 1000, too_long: "%{count} characters is the maximum allowed" }
# Format validations
validates :email, format: { with: /\A[^@\s]+@[^@\s]+\z/, message: "must be a valid email address" }
# Numericality validations
validates :age, numericality: { only_integer: true, greater_than: 0 }
# Uniqueness validations
validates :email, uniqueness: { case_sensitive: false }
validates :username, uniqueness: { scope: :account_id }
# Inclusion/exclusion validations
validates :status, inclusion: { in: %w(active inactive pending) }
validates :fruit, exclusion: { in: %w(apple banana) }
# Custom validations
validate :password_complexity
private
def password_complexity
return if password.blank?
unless password.match(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
errors.add(:password, "must include at least one lowercase letter, one uppercase letter, and one digit")
end
end
end
Validation Helpers
# Conditional validations
validates :card_number, presence: true, if: :paid_with_card?
validates :username, uniqueness: true, unless: :admin?
# Grouped validations
with_options if: :is_admin? do |admin|
admin.validates :password, length: { minimum: 10 }
admin.validates :email, presence: true
end
# On-demand validations
user.valid?(:custom_context)
Error Handling
user = User.new(email: "invalid")
user.valid? # => false
user.errors.full_messages # => ["Email must be a valid email address"]
user.errors.details[:email] # => [{error: :invalid, value: "invalid"}]
# Adding custom errors
errors.add(:base, "This record is invalid")
errors.add(:username, :blank, message: "can't be blank")
Callbacks
Available Callbacks
class User < ApplicationRecord
# Create callbacks
before_validation :normalize_name, on: :create
after_validation :set_location, on: :create
before_save :encrypt_password
around_save :do_something_around
before_create :set_defaults
around_create :do_something_around
after_create :send_welcome_email
# Update callbacks
before_update :update_timestamps
around_update :do_something_around
after_update :update_search_index
# Delete callbacks
before_destroy :check_if_can_destroy
around_destroy :do_something_around
after_destroy :update_statistics
# Common callbacks
after_commit :after_transaction_actions
after_rollback :handle_transaction_rollback
private
def normalize_name
self.name = name.downcase.titleize if name.present?
end
# Other callbacks methods...
end
Callback Options
# Conditional callbacks
before_save :update_status, if: :active?
before_save :update_status, unless: :admin?
before_save :update_status, if: Proc.new { |user| user.active? }
# Callbacks on specific actions
before_validation :normalize_email, on: :create
after_save :send_notification, on: [:create, :update]
# Skip callbacks
user.save(validate: false) # Skips validations
user.update_columns(name: "New name") # Skips callbacks
User.delete_all # Skips callbacks
Migrations
Creating Migrations
# Generate migration
rails generate migration CreateUsers
rails generate migration AddEmailToUsers email:string
rails generate migration RemoveEmailFromUsers email:string
rails generate migration AddDetailsToUsers age:integer active:boolean
# Migration structure
class CreateUsers < ActiveRecord::Migration[7.0]
def change
create_table :users do |t|
t.string :name
t.string :email, null: false
t.integer :age
t.boolean :active, default: true
t.timestamps
end
end
end
Common Migration Methods
# Tables
create_table :users do |t|
# Column definitions
end
drop_table :users
rename_table :old_name, :new_name
# Columns
add_column :users, :email, :string, limit: 100
remove_column :users, :email, :string
rename_column :users, :old_name, :new_name
change_column :users, :age, :integer, default: 0
change_column_null :users, :email, false
change_column_default :users, :active, true
# Indexes
add_index :users, :email, unique: true
add_index :users, [:first_name, :last_name]
remove_index :users, :email
# Foreign keys
add_reference :posts, :user, foreign_key: true
add_reference :posts, :user, foreign_key: { on_delete: :cascade }
remove_reference :posts, :user, foreign_key: true
# Constraints
add_check_constraint :products, "price > 0", name: "price_check"
remove_check_constraint :products, name: "price_check"
Migration Data Types
Type | Description |
---|---|
string | Limited-length string (typically 255 chars) |
text | Unlimited-length text |
integer | Whole number |
float | Floating-point number |
decimal | Precise decimal number (use for money) |
datetime | Date and time |
date | Date only |
time | Time only |
boolean | True or false |
binary | Binary data |
json | JSON data |
jsonb | Better JSON (PostgreSQL only) |
uuid | UUID type (depends on DB) |
Migration Commands
# Run migrations
rails db:migrate
# Rollback migrations
rails db:rollback
rails db:rollback STEP=3
# Reset database
rails db:reset
# Create and migrate database
rails db:create
rails db:migrate
# Migration status
rails db:migrate:status
# Run specific migration
rails db:migrate:up VERSION=20230101000000
rails db:migrate:down VERSION=20230101000000
Transactions
# Basic transaction
ActiveRecord::Base.transaction do
user.update!(name: "New Name")
post.update!(title: "New Title")
end
# Transaction with error handling
begin
ActiveRecord::Base.transaction do
user.update!(name: "New Name")
post.update!(title: "New Title")
end
# Success case
puts "Transaction completed successfully!"
rescue ActiveRecord::RecordInvalid => e
# Handle validation errors
puts "Transaction failed: #{e.message}"
end
# Nested transactions
User.transaction do
user.update!(name: "New Name")
Post.transaction(requires_new: true) do
post.update!(title: "New Title")
end
end
# Transaction callbacks
after_commit { |record| record.update_search_index }
after_rollback { |record| record.notify_admin }
Advanced Features
Single Table Inheritance (STI)
# Base model
class Vehicle < ApplicationRecord
end
# Subclasses
class Car < Vehicle
end
class Truck < Vehicle
end
# Querying
Vehicle.all # Returns all vehicles
Car.all # Returns only cars
Enums
class Order < ApplicationRecord
enum status: {
pending: 0,
processing: 1,
completed: 2,
cancelled: 3
}
enum payment_method: {
credit_card: 'cc',
paypal: 'pp',
bank_transfer: 'bt'
}, _prefix: true
end
# Using enums
order = Order.new
order.pending? # => true
order.status # => "pending"
order.processing! # Sets status to "processing"
Order.completed # Returns all completed orders
order.payment_method_paypal? # => true (with prefix)
Concerns
# Define a concern
module Searchable
extend ActiveSupport::Concern
included do
scope :search, ->(query) { where("name LIKE ?", "%#{query}%") }
end
class_methods do
def search_by_email(email)
where(email: email)
end
end
def generate_search_terms
# Instance method implementation
end
end
# Use the concern
class User < ApplicationRecord
include Searchable
end
User.search("john")
User.search_by_email("john@example.com")
user.generate_search_terms
Query Optimization
# Using pluck (returns an array, not ActiveRecord objects)
User.pluck(:name) # => ["John", "Jane", ...]
User.pluck(:id, :name) # => [[1, "John"], [2, "Jane"], ...]
# Using select (returns ActiveRecord objects with only selected attributes)
User.select(:id, :name)
# Using find_each for batches
User.find_each(batch_size: 100) do |user|
# Process each user
end
# Using find_in_batches
User.find_in_batches(batch_size: 100) do |users|
# Process batch of users
end
# Using in_batches for batch operations
User.in_batches(of: 100) do |batch|
batch.update_all(status: "active")
end
# Eager loading
User.includes(:posts).limit(10) # Avoids N+1 queries
User.includes(posts: [:comments, :tags]).limit(10) # Nested associations
Common Challenges & Solutions
N+1 Query Problem
# Bad (N+1 queries)
users = User.all
users.each do |user|
puts user.posts.count
end
# Good (single query with counter cache)
add_column :users, :posts_count, :integer, default: 0
belongs_to :user, counter_cache: true
# Good (single query with joins)
User.joins(:posts).group('users.id').count
Optimistic Locking
# Add lock_version column
add_column :products, :lock_version, :integer, default: 0, null: false
# ActiveRecord handles the rest automatically
product = Product.find(1)
product2 = Product.find(1)
product.update(price: 10)
product2.update(price: 12) # Raises ActiveRecord::StaleObjectError
Scoped Uniqueness
class ProjectMembership < ApplicationRecord
belongs_to :project
belongs_to :user
# User can only be added once to a project
validates :user_id, uniqueness: { scope: :project_id }
end
Custom Primary Keys
# Migration
create_table :orders, id: false do |t|
t.string :order_number, primary_key: true
t.decimal :total
t.timestamps
end
# Model
class Order < ApplicationRecord
self.primary_key = 'order_number'
end
Best Practices
Security
Mass Assignment Protection: Use strong parameters in controllers
# In controller def user_params params.require(:user).permit(:name, :email, :age) end # Usage @user.update(user_params)
SQL Injection Prevention: Use parameterized queries
# Bad (SQL injection risk) User.where("name = '#{params[:name]}'") # Good User.where("name = ?", params[:name]) User.where(name: params[:name])
Data Validation: Always validate user input
validates :email, presence: true, format: { with: /\A[^@\s]+@[^@\s]+\z/ }
Performance
Use Database Indexes: Add indexes for frequently queried columns
add_index :users, :email add_index :posts, [:user_id, :created_at]
Batch Processing: Process large datasets in batches
User.find_each(batch_size: 100) do |user| # Process user end
Counter Caches: Use counter caches for frequently counted associations
belongs_to :user, counter_cache: true
Eager Loading: Avoid N+1 queries with includes
Post.includes(:user, :comments)
Code Organization
Use Scopes: For commonly used queries
scope :active, -> { where(active: true) }
Use Callbacks Wisely: Avoid excessive callbacks, prefer explicit method calls
# Instead of after_create callback for non-critical operations def publish update(published: true) notify_subscribers end
Keep Models Skinny: Extract complex logic to service objects, concerns, or POROs
# Instead of complex model methods class UserRegistrationService def self.register(attributes) # Complex registration logic end end
Use Associations Properly: Choose the right association type
# For owner/child relationships has_many :comments, dependent: :destroy # For optional relationships belongs_to :parent, optional: true
Resources for Further Learning
Official Documentation
- Rails Guides: Active Record Basics
- Rails Guides: Active Record Migrations
- Rails Guides: Active Record Validations
- Rails Guides: Active Record Associations
- Rails Guides: Active Record Query Interface
Books
- “Agile Web Development with Rails” by Sam Ruby
- “The Rails 5 Way” by Obie Fernandez
- “Metaprogramming Ruby” by Paolo Perrotta