Table of Contents
- What is Active Record?
- Core Concepts: ORM, MVC, and Active Record
- Setting Up Active Record
- Active Record Basics
- Advanced Active Record Features
- Querying with Active Record: Beyond Basic CRUD
- Performance Considerations
- Conclusion
- References
What is Active Record?
Active Record is an ORM (Object-Relational Mapping) library for Ruby. Coined from Martin Fowler’s “Active Record” design pattern, it acts as a bridge between Ruby objects and relational database tables. Here’s what that means:
- Database Tables → Ruby Classes: A database table (e.g.,
users) maps to a Ruby class (e.g.,User). - Table Rows → Ruby Objects: A single row in the
userstable becomes an instance of theUserclass. - Table Columns → Object Attributes: Columns like
nameoremailin theuserstable become methods (user.name,user.email) onUserinstances.
Active Record eliminates the need to write raw SQL for common operations. Instead of:
SELECT * FROM users WHERE email = '[email protected]';
You write:
User.find_by(email: '[email protected]')
Originally developed as part of the Ruby on Rails framework, Active Record can also be used standalone in non-Rails Ruby applications (e.g., Sinatra, plain Ruby scripts).
Core Concepts: ORM, MVC, and Active Record
To understand Active Record’s role, let’s ground it in two key concepts:
1. ORM (Object-Relational Mapping)
ORM is a programming technique that converts data between incompatible systems (object-oriented Ruby and relational databases). Active Record handles:
- Database Connections: Managing connections to the database (e.g., PostgreSQL, MySQL).
- Query Generation: Translating Ruby method calls into SQL queries.
- Result Mapping: Converting SQL result sets into Ruby objects.
2. MVC (Model-View-Controller)
In Rails and many Ruby frameworks, Active Record powers the Model layer of the MVC architecture:
- Model (Active Record): Manages data, business logic, and database interactions.
- View: Handles presentation (e.g., HTML, JSON).
- Controller: Mediates between Model and View, processing user input.
Active Record ensures the Model layer is decoupled from the database, making your code more maintainable.
Setting Up Active Record
Active Record can be used in two ways: within a Rails application (the most common scenario) or standalone. Let’s cover both.
Rails vs. Standalone Active Record
In Rails
Rails includes Active Record by default. When you generate a new Rails app, it configures Active Record automatically:
rails new myapp -d postgresql # Uses PostgreSQL; replace with mysql/sqlite3
cd myapp
rails db:create # Creates the database
Standalone (Non-Rails)
To use Active Record in a non-Rails app (e.g., Sinatra, a script), add these gems to your Gemfile:
# Gemfile
source 'https://rubygems.org'
gem 'activerecord' # Core Active Record
gem 'sqlite3' # Database adapter (use 'pg' for PostgreSQL, 'mysql2' for MySQL)
gem 'rake' # For running migrations
Install gems with bundle install, then configure the database connection.
Database Configuration
Active Record needs to know how to connect to your database. For standalone apps, create a config/database.yml file:
# config/database.yml
default: &default
adapter: sqlite3
pool: 5
timeout: 5000
development:
<<: *default
database: db/development.sqlite3
test:
<<: *default
database: db/test.sqlite3
production:
<<: *default
database: db/production.sqlite3
Then, establish the connection in your code:
# config/environment.rb
require 'active_record'
require 'yaml'
# Load database config
db_config = YAML.load_file('config/database.yml')
ActiveRecord::Base.establish_connection(db_config['development'])
Active Record Basics
Now that we’re set up, let’s dive into the fundamentals: migrations, models, and CRUD operations.
Migrations: Version-Controlling Your Schema
Migrations are Ruby scripts that version-control your database schema. Instead of manually editing SQL to create tables, you write migrations to add/remove tables/columns, and Active Record updates the schema for you.
Why Migrations?
- Collaboration: Teams can share schema changes via versioned files.
- Rollbacks: Undo changes with
rails db:rollback(orrake db:rollbackstandalone). - History: Track how the schema evolved over time.
Creating a Migration
In Rails, generate a migration with:
rails generate migration CreateUsers name:string email:string age:integer
This creates a file like db/migrate/[timestamp]_create_users.rb:
class CreateUsers < ActiveRecord::Migration[7.0]
def change
create_table :users do |t|
t.string :name
t.string :email
t.integer :age
t.timestamps # Adds `created_at` and `updated_at` columns automatically
end
end
end
t.string :name: Creates aVARCHARcolumn forname.t.timestamps: Addscreated_at(when the row was inserted) andupdated_at(when it was last updated).
Running Migrations
Execute the migration to create the users table:
rails db:migrate # Rails
# OR (standalone)
rake db:migrate
Active Record tracks migrations in a schema_migrations table, ensuring each migration runs only once.
Models: Mapping Ruby Classes to Database Tables
A model is a Ruby class that inherits from ActiveRecord::Base. It maps to a database table and provides methods to interact with that table.
Naming Conventions
Active Record uses conventions to avoid boilerplate:
- Model Name: Singular, CamelCase (e.g.,
User). - Table Name: Plural, snake_case (e.g.,
users). - Primary Key: By default,
id(auto-incrementing integer).
If you follow these conventions, no extra configuration is needed!
Example Model
Create a User model that maps to the users table:
# app/models/user.rb (Rails) or models/user.rb (standalone)
class User < ActiveRecord::Base
# No code needed here—Active Record handles the rest!
end
Now, User instances have methods for all columns in the users table:
user = User.new
user.name = "Alice"
user.email = "[email protected]"
user.age = 30
user.save # Saves to the database
CRUD Operations: Create, Read, Update, Delete
Active Record simplifies the four core database operations:
1. Create
Add new records to the database:
# Option 1: new + save (validates before saving)
user = User.new(name: "Bob", email: "[email protected]")
user.age = 25
user.save # Returns true if saved, false otherwise
# Option 2: create (new + save in one step)
user = User.create(name: "Charlie", email: "[email protected]", age: 30)
2. Read
Retrieve records from the database:
# Find by ID
user = User.find(1) # Raises ActiveRecord::RecordNotFound if not found
# Find by attributes (returns first match)
user = User.find_by(email: "[email protected]") # Returns nil if not found
# Find all records
all_users = User.all # Returns an ActiveRecord::Relation (chainable)
# Filter with conditions
adults = User.where("age >= ?", 18) # SQL-like conditions
# Or hash syntax (safer, prevents SQL injection)
adults = User.where(age: 18..Float::INFINITY)
# Sort and limit
sorted_users = User.order(created_at: :desc).limit(10) # 10 most recent users
3. Update
Modify existing records:
user = User.find_by(email: "[email protected]")
user.age = 26
user.save # Updates the record
# Or update attributes directly
user.update(age: 26, name: "Robert") # Returns true/false
4. Delete
Remove records from the database:
user = User.find(1)
user.destroy # Deletes the record and runs callbacks
Advanced Active Record Features
Active Record offers powerful tools beyond basic CRUD. Let’s explore key advanced features.
Associations: Defining Relationships Between Models
Most applications have related data (e.g., users have posts, orders belong to users). Active Record associations model these relationships with simple macros:
Common Associations
| Association | Use Case | Example |
|---|---|---|
belongs_to | One record belongs to another (child) | Post belongs_to :user |
has_many | One record has many others (parent) | User has_many :posts |
has_one | One record has one other (unique parent) | User has_one :profile |
has_many :through | Many-to-many via a join table | User has_many :tags, through: :taggings |
Example: User and Post
- Set up migrations (add foreign keys):
# db/migrate/[timestamp]_create_posts.rb
class CreatePosts < ActiveRecord::Migration[7.0]
def change
create_table :posts do |t|
t.string :title
t.text :content
t.references :user, foreign_key: true # Adds user_id column (foreign key)
t.timestamps
end
end
end
- Define associations in models:
# app/models/user.rb
class User < ActiveRecord::Base
has_many :posts # A user can have many posts
end
# app/models/post.rb
class Post < ActiveRecord::Base
belongs_to :user # A post belongs to one user
end
- Use the associations:
user = User.create(name: "Alice")
post = user.posts.create(title: "Hello World", content: "My first post!") # Automatically sets user_id
# Access associated records
user.posts # Returns all posts by the user
post.user # Returns the user who wrote the post
Validations: Ensuring Data Integrity
Validations ensure data stored in the database meets criteria (e.g., “email can’t be blank”). Active Record runs validations before saving/updating, blocking invalid data.
Example Validations
class User < ActiveRecord::Base
# Ensure name and email are present
validates :name, presence: true
validates :email, presence: true, uniqueness: true # Email must be unique
# Validate email format
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP, message: "must be a valid email" }
# Validate age is a number between 13 and 120
validates :age, numericality: { only_integer: true, in: 13..120 }
end
Check validity and errors:
user = User.new(name: "", email: "invalid-email")
user.valid? # Returns false
user.errors.full_messages # ["Name can't be blank", "Email is invalid"]
Callbacks: Automating Logic During Lifecycle Events
Callbacks let you run code at specific points in an object’s lifecycle (e.g., before saving, after deleting).
Common Callbacks
| Callback | Triggered When… |
|---|---|
before_save | Before save or update |
after_create | After a new record is created |
before_destroy | Before a record is deleted |
Example: Generate a Slug Before Saving
class Post < ActiveRecord::Base
before_save :generate_slug # Run generate_slug before saving
private
def generate_slug
# Convert title to URL-friendly slug (e.g., "Hello World" → "hello-world")
self.slug = title.downcase.gsub(/\s+/, '-').gsub(/[^\w-]/, '')
end
end
Scopes: Reusable Queries
Scopes are named, reusable queries that simplify filtering records. They return an ActiveRecord::Relation, so you can chain them.
Example Scopes
class User < ActiveRecord::Base
# Users over 18
scope :adults, -> { where("age >= ?", 18) }
# Users who signed up in the last 30 days
scope :recent, -> { where("created_at >= ?", 30.days.ago) }
# Chainable scopes
scope :active, -> { where(active: true) }
end
# Usage
adult_users = User.adults.recent.active # All active, recent adults
Querying with Active Record: Beyond Basic CRUD
Active Record’s query interface is powerful—let’s explore advanced querying techniques.
Filtering, Sorting, and Limiting Results
Chain methods to build complex queries:
# Filter and sort
User.where(active: true)
.where.not(age: nil)
.order(age: :asc)
.limit(5) # 5 youngest active users with age set
# Select specific columns (reduces data transfer)
User.select(:name, :email).where(age: 20..25)
Joining Tables and Avoiding N+1 Queries
When querying associated records, Active Record can accidentally trigger the N+1 query problem (1 query to fetch parents, N queries to fetch children).
Example: N+1 Problem
# Fetch all users and their posts (triggers N+1 queries)
users = User.all
users.each do |user|
puts user.posts.count # 1 query per user!
end
Fix with includes
Use includes to eager-load associations in a single query:
# Eager-load posts for all users (2 queries total: 1 for users, 1 for posts)
users = User.includes(:posts).all
users.each do |user|
puts user.posts.count # No extra queries!
end
Performance Considerations
To keep your Active Record queries fast:
-
Index Foreign Keys: Add indexes to columns used in
where,order, orjoinclauses (e.g.,user_idinposts).# In a migration add_index :posts, :user_id # Speeds up queries filtering by user_id -
Avoid
SELECT *: Useselectto fetch only needed columns. -
Batch Processing: For large datasets, use
find_eachto load records in batches:User.find_each(batch_size: 1000) do |user| # Processes 1000 users at a time # Do work end -
Use
pluckfor Single Columns: Fetch raw values instead of objects:user_emails = User.pluck(:email) # Returns an array of emails (no User objects)
Conclusion
Active Record is a cornerstone of Ruby database interactions, transforming tedious SQL into elegant Ruby code. By abstracting database logic into models, leveraging conventions, and providing tools like migrations, associations, and validations, it lets you focus on building features instead of writing SQL.
Whether you’re using Rails or a standalone Ruby app, Active Record simplifies data management while maintaining flexibility for complex queries. With best practices like eager loading and indexing, you can keep your applications performant even as they scale.