Ruby on Rails
02 / 04

ActiveRecord & Migrations

ActiveRecord & Migrations

ActiveRecord is Rails's ORM — it maps classes to tables and objects to rows. Mastering associations, scopes, validations, and the query interface covers the full lifecycle of data in a Rails application.

Associations & Scopes

# app/models/user.rb
class User < ApplicationRecord
  # Associations
  has_many :posts, dependent: :destroy
  has_many :comments, dependent: :destroy
  has_one :profile, dependent: :destroy
  has_many :likes
  has_many :liked_posts, through: :likes, source: :post  # many-to-many via join table

  # Validations
  validates :email, presence: true, uniqueness: { case_sensitive: false },
                    format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :username, presence: true, length: { minimum: 3, maximum: 30 },
                       format: { with: /A[a-z0-9_]+z/, message: "only lowercase letters, numbers, underscores" }
  validates :age, numericality: { greater_than_or_equal_to: 18 }, allow_nil: true

  # Callbacks
  before_save :normalize_email
  after_create :send_welcome_email

  # Scopes — reusable query fragments
  scope :active,     -> { where(active: true) }
  scope :admins,     -> { where(role: :admin) }
  scope :recent,     -> { order(created_at: :desc) }
  scope :by_country, ->(country) { where(country: country) }

  private

  def normalize_email
    self.email = email.downcase.strip
  end

  def send_welcome_email
    WelcomeMailer.with(user: self).welcome.deliver_later
  end
end

# app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
  has_many :comments, dependent: :destroy
  has_many :taggings
  has_many :tags, through: :taggings

  validates :title, presence: true, length: { maximum: 255 }
  validates :body,  presence: true

  scope :published, -> { where(published: true) }
  scope :draft,     -> { where(published: false) }
end

Queries & Migrations

# ActiveRecord query interface
User.all                                          # All users (returns ActiveRecord::Relation)
User.find(1)                                      # By primary key (raises if not found)
User.find_by(email: "alice@example.com")          # First match or nil
User.where(active: true).order(:name).limit(20)
User.where("created_at > ?", 1.week.ago)
User.where(role: [:admin, :moderator])            # IN (:admin, :moderator)
User.where.not(role: :banned)

# Eager loading (avoid N+1 queries)
# N+1: posts.each { |p| p.user.name }   — fires 1 + N queries
Post.includes(:user, :tags).all           # 3 queries total
Post.eager_load(:user).where(users: { active: true })  # JOIN — allows WHERE on association
Post.preload(:comments)                   # Always uses separate queries (no JOINs)

# Select, group, count, pluck
User.count
User.where(active: true).count
User.group(:country).count                # { "US" => 120, "UK" => 45 }
User.pluck(:id, :email)                   # Returns raw array, not AR objects (fast)
User.select(:id, :email)                  # Returns AR objects with only those attrs

# update_all / delete_all (no callbacks or validations!)
Post.where(published: false).update_all(archived: true)
Post.where("created_at < ?", 6.months.ago).delete_all

# Transactions
ActiveRecord::Base.transaction do
  account.update!(balance: account.balance - 100)
  recipient.update!(balance: recipient.balance + 100)
end
# If either update fails, both are rolled back
# Generate a migration
rails generate migration AddPublishedToArticles published:boolean
rails generate migration CreateProducts name:string price:decimal stock:integer
rails generate migration AddIndexToUsersEmail

# Migration file — db/migrate/20240115123456_add_published_to_articles.rb
# class AddPublishedToArticles < ActiveRecord::Migration[7.1]
#   def change
#     add_column :articles, :published, :boolean, default: false, null: false
#     add_index  :articles, :published
#     add_index  :articles, [:user_id, :published]   # composite index
#     add_column :articles, :published_at, :datetime
#     remove_column :articles, :legacy_flag, :boolean  # pass old type for reversibility
#   end
# end

# Database commands
rails db:migrate                # Run pending migrations
rails db:rollback               # Roll back last migration
rails db:rollback STEP=3        # Roll back last 3 migrations
rails db:migrate:status         # Show migration status
rails db:seed                   # Run db/seeds.rb
rails db:reset                  # Drop, create, migrate, seed
rails db:schema:load            # Load schema.rb directly (faster than running all migrations)

# Rails console
rails console                   # Interactive Ruby with Rails loaded
rails console --sandbox         # Rolls back all changes on exit

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free