ActiveRecord Rails Ultimate Cheatsheet: Master Your Database Interactions

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

ConceptDescription
Model Definitionclass User < ApplicationRecord
Table NamingPlural, snake_case (e.g., users table for User model)
Primary KeyDefaults to id (integer or UUID)
Timestampscreated_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

TypeDescription
stringLimited-length string (typically 255 chars)
textUnlimited-length text
integerWhole number
floatFloating-point number
decimalPrecise decimal number (use for money)
datetimeDate and time
dateDate only
timeTime only
booleanTrue or false
binaryBinary data
jsonJSON data
jsonbBetter JSON (PostgreSQL only)
uuidUUID 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

  1. 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)
    
  2. 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])
    
  3. Data Validation: Always validate user input

    validates :email, presence: true, format: { with: /\A[^@\s]+@[^@\s]+\z/ }
    

Performance

  1. Use Database Indexes: Add indexes for frequently queried columns

    add_index :users, :email
    add_index :posts, [:user_id, :created_at]
    
  2. Batch Processing: Process large datasets in batches

    User.find_each(batch_size: 100) do |user|
      # Process user
    end
    
  3. Counter Caches: Use counter caches for frequently counted associations

    belongs_to :user, counter_cache: true
    
  4. Eager Loading: Avoid N+1 queries with includes

    Post.includes(:user, :comments)
    

Code Organization

  1. Use Scopes: For commonly used queries

    scope :active, -> { where(active: true) }
    
  2. 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
    
  3. 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
    
  4. 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

Books

  • “Agile Web Development with Rails” by Sam Ruby
  • “The Rails 5 Way” by Obie Fernandez
  • “Metaprogramming Ruby” by Paolo Perrotta

Online Courses

Blogs and Websites

Scroll to Top