Ruby Active Record — ORM Queries Migrations and Relationships Explained
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 }
4. Not Using Transactions for Related Operations
# 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