Use when active Record patterns including models, associations, queries, validations, and callbacks.
Limited to specific tools
Additional assets for this skill
This skill is limited to using the following tools:
name: rails-active-record-patterns description: Use when active Record patterns including models, associations, queries, validations, and callbacks. allowed-tools:
Master Active Record patterns for building robust Rails models with proper associations, validations, scopes, and query optimization.
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.
# 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
# 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'] %>
# 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
# 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
# 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)
# 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
# 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
# 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
# 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
# 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
# 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
# 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