ð rails-active-record-patterns
Use when active Record patterns including models, associations, queries, validations, and callbacks.
Overview
Master Active Record patterns for building robust Rails models with proper associations, validations, scopes, and query optimization.
Overview
Active Record is Rails' Object-Relational Mapping (ORM) layer that connects model classes to database tables. It implements the Active Record pattern, where each object instance represents a row in the database and includes both data and behavior.
Installation and Setup
Creating Models
# Generate a model with migrations
rails generate model User name:string email:string:uniq
# Generate model with associations
rails generate model Post title:string body:text user:references
# Run migrations
rails db:migrate
Database Configuration
# config/database.yml
default: &default
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
development:
<<: *default
database: myapp_development
test:
<<: *default
database: myapp_test
production:
<<: *default
database: myapp_production
username: myapp
password: <%= ENV['MYAPP_DATABASE_PASSWORD'] %>
Core Patterns
1. Basic Model Definition
# app/models/user.rb
class User < ApplicationRecord
# Validations
validates :email, presence: true, uniqueness: true,
format: { with: URI::MailTo::EMAIL_REGEXP }
validates :name, presence: true, length: { minimum: 2, maximum: 50 }
# Callbacks
before_save :normalize_email
after_create :send_welcome_email
# Scopes
scope :active, -> { where(active: true) }
scope :recent, -> { order(created_at: :desc).limit(10) }
private
def normalize_email
self.email = email.downcase.strip
end
def send_welcome_email
UserMailer.welcome(self).deliver_later
end
end
2. Associations
# app/models/user.rb
class User < ApplicationRecord
# One-to-many
has_many :posts, dependent: :destroy
has_many :comments, dependent: :destroy
# Many-to-many through join table
has_many :memberships, dependent: :destroy
has_many :organizations, through: :memberships
# Has-one
has_one :profile, dependent: :destroy
# Polymorphic association
has_many :images, as: :imageable, dependent: :destroy
end
# app/models/post.rb
class Post < ApplicationRecord
belongs_to :user
has_many :comments, dependent: :destroy
has_many :commenters, through: :comments, source: :user
# Counter cache
belongs_to :user, counter_cache: true
end
# app/models/organization.rb
class Organization < ApplicationRecord
has_many :memberships, dependent: :destroy
has_many :users, through: :memberships
end
# app/models/membership.rb
class Membership < ApplicationRecord
belongs_to :user
belongs_to :organization
enum role: { member: 0, admin: 1, owner: 2 }
end
3. Advanced Queries
# app/models/post.rb
class Post < ApplicationRecord
# Scopes with arguments
scope :by_author, ->(user_id) { where(user_id: user_id) }
scope :published_after, ->(date) { where('published_at > ?', date) }
scope :with_tag, ->(tag) { joins(:tags).where(tags: { name: tag }) }
# Class methods for complex queries
def self.popular(threshold = 100)
where('views_count >= ?', threshold)
.order(views_count: :desc)
end
def self.search(query)
where('title ILIKE ? OR body ILIKE ?', "%#{query}%", "%#{query}%")
end
# Query with joins and includes
def self.with_user_and_comments
includes(:user, comments: :user)
.order(created_at: :desc)
end
end
# Usage
Post.published_after(1.week.ago)
.by_author(current_user.id)
.with_tag('rails')
.popular(50)
4. Validations
# app/models/user.rb
class User < ApplicationRecord
# Presence validation
validates :email, :name, presence: true
# Uniqueness validation
validates :email, uniqueness: { case_sensitive: false }
# Format validation
validates :username, format: {
with: /\A[a-z0-9_]+\z/,
message: "only allows lowercase letters, numbers, and underscores"
}
# Length validation
validates :bio, length: { maximum: 500 }
validates :password, length: { minimum: 8 }, if: :password_required?
# Numericality validation
validates :age, numericality: {
only_integer: true,
greater_than_or_equal_to: 18,
less_than: 120
}
# Custom validation
validate :email_domain_allowed
private
def email_domain_allowed
return if email.blank?
domain = email.split('@').last
unless ALLOWED_DOMAINS.include?(domain)
errors.add(:email, "domain #{domain} is not allowed")
end
end
def password_required?
new_record? || password.present?
end
end
5. Callbacks
# app/models/post.rb
class Post < ApplicationRecord
# Before callbacks
before_validation :normalize_title
before_save :calculate_reading_time
before_create :generate_slug
# After callbacks
after_create :notify_followers
after_update :clear_cache, if: :saved_change_to_body?
after_destroy :cleanup_attachments
# Around callbacks
around_save :log_save_time
private
def normalize_title
self.title = title.strip.titleize if title.present?
end
def calculate_reading_time
return unless body_changed?
words = body.split.size
self.reading_time = (words / 200.0).ceil
end
def generate_slug
self.slug = title.parameterize
end
def notify_followers
NotifyFollowersJob.perform_later(self)
end
def clear_cache
Rails.cache.delete("post/#{id}")
end
def cleanup_attachments
attachments.purge_later
end
def log_save_time
start = Time.current
yield
duration = Time.current - start
Rails.logger.info "Post #{id} saved in #{duration}s"
end
end
6. Enum Patterns
# app/models/post.rb
class Post < ApplicationRecord
# Basic enum
enum status: {
draft: 0,
published: 1,
archived: 2
}
# Enum with prefix/suffix
enum visibility: {
public: 0,
private: 1,
unlisted: 2
}, _prefix: :visibility
# Multiple enums
enum content_type: {
article: 0,
video: 1,
podcast: 2
}, _suffix: :content
# Scopes automatically created
# Post.draft, Post.published, Post.archived
# Post.visibility_public, Post.visibility_private
# Post.article_content, Post.video_content
# Query methods
# post.draft?, post.published?, post.archived?
# post.visibility_public?, post.visibility_private?
# State transitions
def publish!
published! if draft?
end
end
7. Query Optimization
# app/models/post.rb
class Post < ApplicationRecord
# Eager loading to avoid N+1
scope :with_associations, -> {
includes(:user, :tags, comments: :user)
}
# Select specific columns
scope :title_and_author, -> {
select('posts.id, posts.title, users.name as author_name')
.joins(:user)
}
# Batch processing
def self.process_in_batches
find_each(batch_size: 1000) do |post|
post.process
end
end
# Pluck for arrays
def self.recent_titles
order(created_at: :desc)
.limit(10)
.pluck(:title)
end
# Exists check (efficient)
def self.has_recent_posts?(user_id)
where(user_id: user_id)
.where('created_at > ?', 1.day.ago)
.exists?
end
# Count with joins
def self.popular_authors
joins(:user)
.group('users.id', 'users.name')
.select('users.id, users.name, COUNT(posts.id) as posts_count')
.having('COUNT(posts.id) >= ?', 10)
.order('posts_count DESC')
end
end
8. Transactions
# app/services/post_publisher.rb
class PostPublisher
def self.publish(post, user)
ActiveRecord::Base.transaction do
post.update!(status: :published, published_at: Time.current)
user.increment!(:posts_count)
NotificationService.notify_followers(post)
# If any operation fails, entire transaction is rolled back
end
rescue ActiveRecord::RecordInvalid => e
Rails.logger.error "Failed to publish post: #{e.message}"
false
end
# Nested transactions with savepoints
def self.complex_operation(post)
ActiveRecord::Base.transaction do
post.update!(featured: true)
ActiveRecord::Base.transaction(requires_new: true) do
# This creates a savepoint
post.tags.create!(name: 'featured')
end
end
end
end
9. STI (Single Table Inheritance)
# app/models/vehicle.rb
class Vehicle < ApplicationRecord
validates :make, :model, presence: true
def max_speed
raise NotImplementedError
end
end
# app/models/car.rb
class Car < Vehicle
validates :doors, presence: true
def max_speed
120
end
end
# app/models/motorcycle.rb
class Motorcycle < Vehicle
validates :engine_size, presence: true
def max_speed
180
end
end
# Usage
car = Car.create(make: 'Toyota', model: 'Camry', doors: 4)
car.type # => "Car"
Vehicle.all # Returns both cars and motorcycles
Car.all # Returns only cars
10. Concerns
# app/models/concerns/sluggable.rb
module Sluggable
extend ActiveSupport::Concern
included do
before_validation :generate_slug
validates :slug, presence: true, uniqueness: true
end
class_methods do
def find_by_slug(slug)
find_by(slug: slug)
end
end
private
def generate_slug
return if slug.present?
base_slug = title.parameterize
self.slug = unique_slug(base_slug)
end
def unique_slug(base_slug)
slug_candidate = base_slug
counter = 1
while self.class.exists?(slug: slug_candidate)
slug_candidate = "#{base_slug}-#{counter}"
counter += 1
end
slug_candidate
end
end
# app/models/post.rb
class Post < ApplicationRecord
include Sluggable
end
Best Practices
- Use scopes for reusable queries - Keep query logic in the model
- Eager load associations - Prevent N+1 queries with includes/preload
- Add database indexes - Index foreign keys and frequently queried columns
- Use counter caches - Optimize count queries for associations
- Validate at model level - Ensure data integrity with validations
- Keep callbacks simple - Extract complex logic to service objects
- Use transactions - Ensure data consistency for multi-step operations
- Leverage concerns - Share common behavior across models
- Use enums for state - Type-safe state management with enums
- Write efficient queries - Use select, pluck, and exists appropriately
Common Pitfalls
- N+1 queries - Forgetting to eager load associations
- Callback hell - Too many callbacks making flow hard to follow
- Fat models - Putting too much business logic in models
- Missing indexes - Slow queries due to unindexed columns
- Unsafe updates - Not using transactions for related operations
- Validation bypass - Using update_attribute or save(validate: false)
- Memory bloat - Loading all records instead of batching
- SQL injection - Using string interpolation in where clauses
- Counter cache mismatches - Manual updates breaking counter caches
- Ignoring database constraints - Not adding DB-level validations
When to Use
- Building data-backed Rails applications
- Implementing business logic tied to database models
- Creating REST APIs with Rails
- Developing CRUD interfaces
- Managing complex data relationships
- Building multi-tenant applications
- Creating admin interfaces with Active Admin
- Implementing soft deletes and audit trails
- Building reporting and analytics features
- Creating content management systems