Active Record
Active Record is the ORM of Ruby on Rails, implementing the Active Record pattern with minimal configuration through the Convention over Configuration philosophy. It includes built-in database migrations, validations, callbacks, and association features.
GitHub Overview
Topics
Star History
Library
Active Record
Overview
Active Record is the ORM of Ruby on Rails, implementing the Active Record pattern with minimal configuration through the Convention over Configuration philosophy. It includes built-in database migrations, validations, callbacks, and association features.
Details
Active Record faithfully implements the Active Record pattern defined by Martin Fowler. It embodies Rails' "convention over configuration" philosophy, enabling database operations with minimal code by following conventions for table and column naming. It integrates business logic and data access functionality in model classes, providing an intuitive and expressive API.
Key Features
- Convention over Configuration: Minimizing configuration through conventions
- Rich Validations: Validation features to maintain data integrity
- Associations: Intuitive relationship definitions
- Callbacks: Hooks in model lifecycle
- Migrations: Safe schema change management
Pros and Cons
Pros
- Natural and readable API for Ruby developers
- Achieving powerful database operations with minimal configuration
- Rich association features enable concise expression of complex data relationships
- Complete integration with Rails ecosystem
- Stability and reliability through years of proven track record
Cons
- Performance challenges in large-scale systems
- Tight coupling of business logic and data access due to Active Record pattern
- Difficult to use with other frameworks due to Rails framework dependency
- Limited expression for complex queries in some cases
Reference Pages
Code Examples
Installation and Basic Setup
# Create Rails project
rails new myapp
cd myapp
# Database setup (configured in config/database.yml)
rails generate model User name:string email:string
rails generate model Post title:string content:text user:references
# config/database.yml
default: &default
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
development:
<<: *default
database: myapp_development
username: myapp
password: <%= ENV['MYAPP_DATABASE_PASSWORD'] %>
test:
<<: *default
database: myapp_test
production:
<<: *default
database: myapp_production
username: myapp
password: <%= ENV['MYAPP_DATABASE_PASSWORD'] %>
Basic CRUD Operations (Model Definition, Create, Read, Update, Delete)
# app/models/user.rb
class User < ApplicationRecord
has_many :posts, dependent: :destroy
validates :name, presence: true, length: { minimum: 2, maximum: 50 }
validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
before_save :downcase_email
scope :active, -> { where(status: 'active') }
scope :recent, -> { order(created_at: :desc) }
private
def downcase_email
self.email = email.downcase
end
end
# app/models/post.rb
class Post < ApplicationRecord
belongs_to :user
validates :title, presence: true, length: { minimum: 5, maximum: 100 }
validates :content, presence: true, length: { minimum: 10 }
scope :published, -> { where.not(published_at: nil) }
scope :by_date, -> { order(created_at: :desc) }
def published?
published_at.present?
end
end
# Run migrations
# rails db:migrate
# Basic CRUD operations
# Create
user = User.create(
name: 'John Doe',
email: '[email protected]'
)
# Read
all_users = User.all
user_by_id = User.find(1)
user_by_email = User.find_by(email: '[email protected]')
active_users = User.active
# Update
user = User.find(1)
user.update(name: 'John Smith')
# Bulk update
User.where(status: 'pending').update_all(status: 'active')
# Delete
user = User.find(1)
user.destroy
# Bulk delete
User.where(status: 'inactive').destroy_all
Advanced Queries and Relationships
# Complex conditional queries
active_users = User.where(status: 'active')
.where('created_at > ?', 30.days.ago)
.where('email LIKE ?', '%@company.com')
.order(created_at: :desc)
.limit(10)
# Association queries
users_with_posts = User.includes(:posts)
users_with_recent_posts = User.joins(:posts)
.where(posts: { created_at: 1.week.ago.. })
.distinct
# Nested associations
users_with_published_posts = User.joins(posts: :comments)
.where(posts: { published_at: ..Time.current })
.where(comments: { approved: true })
# Aggregation queries
user_stats = User.select('COUNT(*) as total_users, AVG(posts_count) as avg_posts')
.joins(:posts)
.group('users.id')
# Count queries
User.joins(:posts).group('users.id').count
Post.group(:user_id).count
# Subqueries
active_users = User.where(id: Post.select(:user_id).where('created_at > ?', 1.month.ago))
# Complex scopes
class User < ApplicationRecord
scope :with_recent_activity, ->(days = 30) {
joins(:posts).where(posts: { created_at: days.days.ago.. }).distinct
}
scope :by_post_count, ->(min_count = 1) {
joins(:posts).group('users.id').having('COUNT(posts.id) >= ?', min_count)
}
end
# Using scopes
active_productive_users = User.active
.with_recent_activity(7)
.by_post_count(3)
# Dynamic queries
def search_users(params)
scope = User.all
scope = scope.where('name ILIKE ?', "%#{params[:name]}%") if params[:name].present?
scope = scope.where(status: params[:status]) if params[:status].present?
scope = scope.where('created_at >= ?', params[:created_after]) if params[:created_after].present?
scope
end
Migrations and Schema Management
# Create migrations
rails generate migration CreateUsers name:string email:string:index
rails generate migration AddStatusToUsers status:string
# Run migrations
rails db:migrate
# Rollback
rails db:rollback
rails db:rollback STEP=3
# Check migration status
rails db:migrate:status
# db/migrate/xxxx_create_users.rb
class CreateUsers < ActiveRecord::Migration[7.0]
def change
create_table :users do |t|
t.string :name, null: false
t.string :email, null: false
t.string :status, default: 'active'
t.timestamps
end
add_index :users, :email, unique: true
add_index :users, [:status, :created_at]
end
end
# db/migrate/xxxx_create_posts.rb
class CreatePosts < ActiveRecord::Migration[7.0]
def change
create_table :posts do |t|
t.string :title, null: false
t.text :content
t.references :user, null: false, foreign_key: true
t.datetime :published_at
t.timestamps
end
add_index :posts, [:user_id, :published_at]
add_index :posts, :published_at
end
end
# Custom migration
class AddFullTextSearchToPosts < ActiveRecord::Migration[7.0]
def up
execute "CREATE INDEX posts_full_text_search ON posts USING gin(to_tsvector('english', title || ' ' || content))"
end
def down
execute "DROP INDEX posts_full_text_search"
end
end
# Data migration
class MigrateUserStatuses < ActiveRecord::Migration[7.0]
def up
User.where(last_login_at: nil).update_all(status: 'inactive')
User.where('last_login_at < ?', 1.year.ago).update_all(status: 'dormant')
end
def down
User.where(status: ['inactive', 'dormant']).update_all(status: 'active')
end
end
Performance Optimization and Advanced Features
# Transactions
ActiveRecord::Base.transaction do
user = User.create!(
name: 'Jane Doe',
email: '[email protected]'
)
post = Post.create!(
title: 'First Post',
content: 'My first post using Active Record',
user: user,
published_at: Time.current
)
end
# Batch operations
users_data = [
{ name: 'User 1', email: '[email protected]' },
{ name: 'User 2', email: '[email protected]' },
{ name: 'User 3', email: '[email protected]' }
]
User.insert_all(users_data)
# Batch update
User.where(status: 'pending').update_all(
status: 'active',
activated_at: Time.current
)
# Batch processing (find_each)
User.where(status: 'active').find_each(batch_size: 100) do |user|
user.send_newsletter
end
# Avoid N+1 problem with includes
posts = Post.includes(:user, :comments).limit(10)
posts.each do |post|
puts "#{post.title} by #{post.user.name} (#{post.comments.count} comments)"
end
# Counter cache
class User < ApplicationRecord
has_many :posts, dependent: :destroy, counter_cache: true
end
class Post < ApplicationRecord
belongs_to :user, counter_cache: true
end
# Custom validation
class User < ApplicationRecord
validate :email_domain_allowed
private
def email_domain_allowed
allowed_domains = ['company.com', 'partner.org']
domain = email.split('@').last
unless allowed_domains.include?(domain)
errors.add(:email, 'must be from an allowed domain')
end
end
end
# Callbacks
class User < ApplicationRecord
before_validation :normalize_email
before_create :generate_uuid
after_create :send_welcome_email
after_update :log_changes, if: :saved_change_to_email?
private
def normalize_email
self.email = email.strip.downcase
end
def generate_uuid
self.uuid = SecureRandom.uuid
end
def send_welcome_email
UserMailer.welcome(self).deliver_later
end
def log_changes
Rails.logger.info "User #{id} email changed from #{email_previous_change[0]} to #{email}"
end
end
# Custom attributes
class User < ApplicationRecord
attribute :preferences, :json, default: {}
def full_name
"#{first_name} #{last_name}"
end
def full_name=(name)
split = name.split(' ', 2)
self.first_name = split.first
self.last_name = split.last
end
end
Framework Integration and Practical Examples
# Controller integration
class UsersController < ApplicationController
before_action :set_user, only: [:show, :edit, :update, :destroy]
def index
@users = User.includes(:posts)
.page(params[:page])
.per(10)
end
def show
@posts = @user.posts.published.recent.limit(5)
end
def create
@user = User.new(user_params)
if @user.save
redirect_to @user, notice: 'User was successfully created.'
else
render :new
end
end
def update
if @user.update(user_params)
redirect_to @user, notice: 'User was successfully updated.'
else
render :edit
end
end
def destroy
@user.destroy
redirect_to users_url, notice: 'User was successfully deleted.'
end
private
def set_user
@user = User.find(params[:id])
end
def user_params
params.require(:user).permit(:name, :email, :status)
end
end
# API controller
class Api::V1::UsersController < ApiController
before_action :set_user, only: [:show, :update, :destroy]
def index
@users = User.all
render json: @users, each_serializer: UserSerializer
end
def show
render json: @user, serializer: UserDetailSerializer
end
def create
@user = User.new(user_params)
if @user.save
render json: @user, serializer: UserSerializer, status: :created
else
render json: { errors: @user.errors }, status: :unprocessable_entity
end
end
private
def set_user
@user = User.find(params[:id])
end
def user_params
params.require(:user).permit(:name, :email)
end
end
# Background job
class UserDataProcessingJob < ApplicationJob
queue_as :default
def perform(user_id)
user = User.find(user_id)
# Execute heavy processing in background
user.process_analytics_data
user.update(processed_at: Time.current)
end
end
# Job enqueue
UserDataProcessingJob.perform_later(user.id)
# Testing (RSpec)
RSpec.describe User, type: :model do
describe 'validations' do
it { should validate_presence_of(:name) }
it { should validate_presence_of(:email) }
it { should validate_uniqueness_of(:email) }
end
describe 'associations' do
it { should have_many(:posts).dependent(:destroy) }
end
describe '#full_name' do
let(:user) { build(:user, first_name: 'John', last_name: 'Doe') }
it 'returns the full name' do
expect(user.full_name).to eq('John Doe')
end
end
end
# Factory (FactoryBot)
FactoryBot.define do
factory :user do
name { Faker::Name.name }
email { Faker::Internet.unique.email }
status { 'active' }
trait :with_posts do
after(:create) do |user|
create_list(:post, 3, user: user)
end
end
end
factory :post do
title { Faker::Lorem.sentence(word_count: 4) }
content { Faker::Lorem.paragraph(sentence_count: 5) }
association :user
published_at { 1.day.ago }
end
end
# Seeds
# db/seeds.rb
User.create!([
{
name: 'Admin User',
email: '[email protected]',
status: 'active'
},
{
name: 'Test User',
email: '[email protected]',
status: 'active'
}
])
# Performance monitoring for production
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
# Slow query logging
around_action :log_slow_queries
private
def log_slow_queries
start_time = Time.current
yield
duration = Time.current - start_time
if duration > 1.second
Rails.logger.warn "Slow query detected: #{duration}s"
end
end
end