Skip to content

Ruby Active Record — ORM Queries Migrations and Relationships Explained

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Ruby Active Record. We cover key concepts, practical examples, and best practices to help you master this topic.

Ruby Active Record is Rails' object-relational mapping (ORM) layer that maps database tables to Ruby classes, providing CRUD operations, a powerful query interface, relationships, and lifecycle callbacks.

What You'll Learn

  • Creating models and mapping tables
  • CRUD operations with Active Record
  • Query interface methods
  • Model relationships and associations
  • Callbacks and validations

Why It Matters

Active Record is the heart of Rails. Doda Browser uses Active Record patterns for its configuration storage. Durga Antivirus Pro uses Active Record for managing scan schedules, threat databases, and user preferences. Mastery of Active Record is mastery of Rails data layer.

Real-World Use

Every Rails application uses Active Record for database operations — from simple blog posts to complex e-commerce systems with millions of records.

flowchart LR
    A["Active Record"] --> B["Models"]
    B --> C["CRUD"]
    C --> D["Queries"]
    D --> E["Relationships"]
    E --> F["Callbacks"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style F fill:#f1f5f9,stroke:#94a3b8,color:#64748b

Creating Models

bin/rails generate model User name:string email:string age:integer
bin/rails db:migrate

This creates:

# app/models/user.rb
class User < ApplicationRecord
end

And a Migration that creates the users table with columns: name, email, age, plus automatic id, created_at, and updated_at.

CRUD Operations

Create

# Method 1: new + save
user = User.new(name: "Alice", email: "alice@example.com", age: 30)
user.save

# Method 2: create (new + save in one call)
user = User.create(name: "Bob", email: "bob@example.com", age: 25)

# Method 3: create! (raises on failure)
user = User.create!(name: "Charlie", email: "charlie@example.com", age: 35)

# Method 4: find_or_create_by
user = User.find_or_create_by(email: "alice@example.com") do |u|
  u.name = "Alice"
  u.age = 30
end

Read

# Find by id
user = User.find(1)           # Raises if not found
user = User.find_by(id: 1)    # Returns nil if not found

# Find by attributes
user = User.find_by(email: "alice@example.com")
user = User.find_by_name("Alice")  # Dynamic finder

# All records
users = User.all  # ActiveRecord::Relation (lazy)

# First and last
User.first
User.last
User.second
User.third

# Count
User.count          # Total records
User.where(age: 30).count

# Check existence
User.exists?(1)               # true/false
User.exists?(email: "alice@example.com")

Update

# Method 1: save after modifying
user = User.find(1)
user.name = "Alice Smith"
user.save

# Method 2: update
user.update(name: "Alice Smith", age: 31)

# Method 3: update! (raises on failure)
user.update!(name: "Alice Smith")

# Method 4: update_all (mass update)
User.where(age: nil).update_all(age: 0)

# Method 5: increment/decrement
user.increment!(:age)
user.decrement!(:age, 5)

Delete

# Destroy (runs callbacks)
user = User.find(1)
user.destroy

# Delete (skips callbacks, faster)
user.delete

# Mass delete
User.where("age < 18").destroy_all
User.delete_all

Query Interface

Where Clauses

# Simple conditions
User.where(name: "Alice")
User.where("age >= ?", 18)
User.where("age BETWEEN ? AND ?", 20, 40)

# Array syntax
User.where("name LIKE ?", "%Ali%")

# Hash syntax
User.where(name: "Alice", age: 30)
User.where(name: ["Alice", "Bob"])  # IN clause

# NOT
User.where.not(name: "Alice")

# OR
User.where(name: "Alice").or(User.where(name: "Bob"))

Ordering and Limiting

User.order(name: :asc)
User.order(age: :desc, name: :asc)
User.order(:name)  # Ascending by default

User.limit(10)
User.offset(20)

User.order(created_at: :desc).limit(5).offset(10)

Scopes

class User < ApplicationRecord
  scope :active, -> { where(active: true) }
  scope :adults, -> { where("age >= 18") }
  scope :by_name, ->(name) { where(name: name) }
  scope :recent, -> { order(created_at: :desc).limit(10) }
end

# Usage
User.active.adults
User.by_name("Alice").active
User.recent

Pluck and Select

# Pluck — get specific columns as array
emails = User.pluck(:email)           # ["alice@...", "bob@..."]
names = User.where(active: true).pluck(:name, :email)

# Select — SQL-level column selection
User.select(:id, :name).map { |u| u.name }

# Reselect — override default selection
User.reselect(:name)

Model Relationships

belongs_to

# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :article
  belongs_to :user, optional: true  # Allow nil
end

has_many

# app/models/article.rb
class Article < ApplicationRecord
  belongs_to :author, class_name: "User"
  has_many :comments, dependent: :destroy
  has_many :tags, through: :taggings
  has_one :featured_image, as: :imageable, dependent: :destroy
end

has_one

# app/models/user.rb
class User < ApplicationRecord
  has_many :articles
  has_one :profile, dependent: :destroy
  has_one :avatar, through: :profile
end

has_and_belongs_to_many

# app/models/category.rb
class Category < ApplicationRecord
  has_and_belongs_to_many :articles
end

# app/models/article.rb
class Article < ApplicationRecord
  has_and_belongs_to_many :categories
end

Nested Attributes

class User < ApplicationRecord
  has_one :profile
  accepts_nested_attributes_for :profile
end

# Create user with profile in one form
User.create(
  name: "Alice",
  profile_attributes: { bio: "Ruby developer", twitter: "@alice" }
)

Callbacks

class Article < ApplicationRecord
  before_save :set_slug
  after_create :send_notification
  around_update :log_changes

  private

  def set_slug
    self.slug = title.parameterize
  end

  def send_notification
    NotificationService.new_article(self).deliver_later
  end

  def log_changes
    changes = self.changes
    yield
    Rails.logger.info "Article #{id} changed: #{changes}"
  end
end

Available Callbacks

before_validation
after_validation
before_save
around_save
before_create
around_create
after_create
before_update
around_update
after_update
before_destroy
around_destroy
after_destroy
after_commit
after_rollback
after_find
after_initialize

Transaction

# Wrap multiple operations in a transaction
ApplicationRecord.transaction do
  @article = Article.create!(title: "Breaking News")
  @comment = @article.comments.create!(body: "First!")
  NotificationMailer.new_comment(@comment).deliver_now!
end
# If any operation fails, ALL changes are rolled back

N+1 Query Problem

# Bad — N+1 queries
articles = Article.all
articles.each { |a| puts a.author.name }  # 1 query for articles + N for authors

# Good — eager load
articles = Article.includes(:author)
articles.each { |a| puts a.author.name }  # 2 queries total

# Preload vs Eager Load vs Joins
Article.preload(:author)        # Always separate queries
Article.eager_load(:author)     # LEFT OUTER JOIN
Article.joins(:author)          # INNER JOIN (only matching)

Common Mistakes

1. N+1 Queries in Views

<% @articles.each do |article| %>
  <p><%= article.author.name %></p>  <!-- N+1! -->
<% end %>

2. Forgetting to Handle nil on belongs_to

# Bad — crashes if comment has no user
comment.user.name

# Good — safe navigation
comment.user&.name

3. Using each Instead of find_each for Batches

# Bad — loads all records into memory
User.all.each { |u| u.send_newsletter }

# Good — batches 1000 at a time
User.find_each(batch_size: 1000) { |u| u.send_newsletter }
# Bad — comment saved even if notification fails
comment = Comment.create!(body: "Nice!")
Notification.send(comment)  # What if this raises?

# Good — both or neither
Comment.transaction do
  comment = Comment.create!(body: "Nice!")
  Notification.send(comment)
end

5. Overusing Callbacks

# Hard to debug — callbacks fire unexpectedly
class User < ApplicationRecord
  before_save :do_many_things
  after_save :do_more_things
  around_save :wrap_things
end

Practice Questions

1. What is Active Record's role in Rails?

It's the ORM layer that maps database tables to Ruby classes, providing query building, CRUD operations, relationships, and lifecycle management without writing SQL.

2. What's the difference between find and find_by?

find raises ActiveRecord::RecordNotFound if no record matches. find_by returns nil. Use find when the record should exist; use find_by when absence is valid.

3. How do you prevent N+1 queries?

Use includes, preload, or eager_load to load associations in advance. Article.includes(:author) loads all authors in 2 queries instead of N+1.

4. What's the difference between destroy and delete?

destroy runs callbacks (before_destroy, after_destroy) and cascades dependencies. delete skips callbacks and directly removes the row. Use destroy unless you need performance.

Challenge: Build a model layer for a blog with User, Article, Comment, and Tag models with proper relationships, scopes, and a scope that finds articles with the most comments.

Solution
# app/models/user.rb
class User < ApplicationRecord
  has_many :articles, dependent: :destroy
  has_many :comments, dependent: :destroy
  validates :name, presence: true
  validates :email, presence: true, uniqueness: true
end

# app/models/article.rb
class Article < ApplicationRecord
  belongs_to :user
  has_many :comments, dependent: :destroy
  has_and_belongs_to_many :tags

  validates :title, presence: true, length: { minimum: 5 }
  validates :body, presence: true

  scope :published, -> { where.not(published_at: nil) }
  scope :recent, -> { order(created_at: :desc) }
  scope :most_commented, -> {
    left_joins(:comments)
      .group(:id)
      .order("COUNT(comments.id) DESC")
  }

  def publish!
    update(published_at: Time.now)
  end
end

# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :article, counter_cache: true
  belongs_to :user

  validates :body, presence: true
  scope :recent, -> { order(created_at: :desc) }
end

# app/models/tag.rb
class Tag < ApplicationRecord
  has_and_belongs_to_many :articles
  validates :name, presence: true, uniqueness: true
  scope :by_usage, -> {
    left_joins(:articles)
      .group(:id)
      .order("COUNT(articles.id) DESC")
  }
end

FAQ

{{< faq question="What is the difference between Active Record and Data Mapper?" >}} Active Record combines database row and domain object — each model instance corresponds to a row. Data Mapper separates them. Active Record is simpler; Data Mapper is more flexible for complex domains. {{< /faq >}}

{{< faq question="How do I use raw SQL with Active Record?" >}} Use find_by_sql, connection.execute, or Arel for complex queries. User.find_by_sql("SELECT * FROM users WHERE age > 18"). Generally prefer the query interface. {{< /faq >}}

{{< faq question="What is counter_cache?" >}} A column that caches the count of associated records. belongs_to :article, counter_cache: true auto-updates comments_count on the article, avoiding COUNT queries. {{< /faq >}}

{{< faq question="How do I handle soft deletes?" >}} Add a deleted_at column, use a default scope (default_scope { where(deleted_at: nil) }), and override destroy to set deleted_at. Consider the acts_as_paranoid gem. {{< /faq >}}

{{< faq question="What is the difference between has_many :through and has_and_belongs_to_many?" >}} HABTM uses a join table directly. has_many :through uses a separate model, letting you add attributes and callbacks to the join. Prefer has_many :through for flexibility. {{< /faq >}}

Try It Yourself

# active_record_demo.rb
# Run in Rails console (bin/rails console)

require "active_record"

# Create users
alice = User.create!(name: "Alice", email: "alice@example.com", age: 30)
bob = User.create!(name: "Bob", email: "bob@example.com", age: 25)

# Create articles
article1 = alice.articles.create!(
  title: "Getting Started with Rails",
  body: "Rails is amazing..."
)
article2 = alice.articles.create!(
  title: "Active Record Deep Dive",
  body: "Let's explore AR..."
)

# Add comments
article1.comments.create!(user: bob, body: "Great article!")
article1.comments.create!(user: alice, body: "Thanks Bob!")

# Query
puts "Alice's articles: #{alice.articles.count}"
puts "Most commented: #{Article.most_commented.first.title}"
puts "All users: #{User.pluck(:name).join(', ')}"

What's Next

Now that you understand Active Record models, learn about Action Pack — Rails' controller and routing layer for handling HTTP requests.

Topic Description Link
Ruby Action Pack Controllers, routing, params {{< ref "27-action-pack" >}}
Ruby Migrations Schema changes, data types {{< ref "29-migrations" >}}
Python SQLAlchemy Compare Python's ORM Python

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro