iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Active Record

ActiveRecord is Rails ORM: domain objects that map to database rows, with associations, scopes, validations, callbacks, and migrations. The win is high-leverage CRUD without writing SQL; the trap is hidden N+1 queries and over-friendly callbacks that blur business logic into database access.

Models, associations, scopes, joins, eager loading

EXAMPLE
# ===== Migration first — schema is the source of truth =====
# db/migrate/20260618_create_orders.rb
class CreateOrders < ActiveRecord::Migration[7.1]
  def change
    create_table :customers do |t|
      t.string  :name,  null: false
      t.string  :email, null: false, index: { unique: true }
      t.timestamps
    end

    create_table :orders do |t|
      t.references :customer, foreign_key: true, null: false
      t.string  :status,      null: false, default: 'new'
      t.integer :total_cents, null: false
      t.datetime :paid_at
      t.timestamps
      t.index %i[customer_id status]
    end

    create_table :line_items do |t|
      t.references :order, foreign_key: true, null: false
      t.string :sku, null: false
      t.integer :qty, null: false, default: 1
      t.integer :price_cents, null: false
    end
  end
end

# ===== Models — declare relationships + rules =====
class Customer < ApplicationRecord
  has_many :orders, dependent: :restrict_with_exception
  validates :email, presence: true, uniqueness: { case_sensitive: false }
  before_save { self.email = email.downcase }
end

class Order < ApplicationRecord
  belongs_to :customer
  has_many :line_items, dependent: :destroy

  enum status: { new: 'new', paid: 'paid', shipped: 'shipped', cancelled: 'cancelled' }

  validates :total_cents, numericality: { greater_than_or_equal_to: 0 }

  scope :recent, ->(d = 30.days.ago) { where('created_at > ?', d) }
  scope :open,   -> { where(status: %w[new paid]) }

  def total_aud = (total_cents / 100.0).round(2)
end

class LineItem < ApplicationRecord
  belongs_to :order
end

# ===== Queries — the chainable API =====
# Find a customer + their open orders, both in one round trip
customer = Customer
  .includes(orders: :line_items)
  .find_by!(email: 'alice@example.com')

# Aggregate: customers with > $500 lifetime
top = Customer
  .joins(:orders)
  .where(orders: { status: %w[paid shipped] })
  .group('customers.id')
  .having('SUM(orders.total_cents) > ?', 50_000)
  .order('SUM(orders.total_cents) DESC')
  .limit(10)

# Time-bucketed counts
counts = Order.recent.group('DATE(created_at)').count

# ===== Writes — atomically =====
ActiveRecord::Base.transaction do
  o = Customer.find(42).orders.create!(status: 'new')
  o.line_items.create!(sku: 'sku-1', qty: 2, price_cents: 1995)
  o.update!(total_cents: o.line_items.sum('qty * price_cents'))
end

# ===== N+1 detection + fix =====
# BAD — fires 1 query for the customers + N queries for each set of orders
Customer.where(id: ids).each { |c| puts c.orders.size }

# GOOD — one extra query for the orders, joined in memory
Customer.includes(:orders).where(id: ids).each { |c| puts c.orders.size }

# Enable the bullet gem in dev to flag missing includes in tests + browser bar.

# ===== Callbacks — use carefully =====
# Rule of thumb: callbacks for DATA INTEGRITY only.
# Side effects (email, payment, audit) belong in service objects or jobs;
# callbacks make them implicit and untestable in isolation.

# Wrong:
class Order < ApplicationRecord
  after_save :send_receipt        # sends email even for tests, imports, rebuilds
end

# Better:
class PlaceOrder
  def self.call(customer:, items:)
    Order.transaction do
      o = customer.orders.create!(total_cents: items.sum { |i| i[:price_cents] * i[:qty] })
      items.each { |i| o.line_items.create!(i) }
      OrderMailer.with(order: o).placed.deliver_later
      o
    end
  end
end

Why it matters

`includes(:assoc)` is the single biggest performance lever in ActiveRecord. Forget it on a list page and you get N+1 queries the moment the view touches an association. Add the bullet gem in dev, fail tests when it flags an N+1, and N+1 stops sneaking into production.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
class User < ApplicationRecord
    has_many :posts
    validates :email, presence: true, uniqueness: true
end
User.where(active: true).order(:name)
Try it Yourself »

Discussion

Loading…