rails-models
SKILL.md
Rails Models (ActiveRecord)
Quick Reference
| Pattern | Example |
|---|---|
| Model Generation | rails g model User name:string email:string |
| Migration | rails g migration AddAgeToUsers age:integer |
| Validation | validates :email, presence: true, uniqueness: true |
| Association | has_many :posts, dependent: :destroy |
| Callback | before_save :normalize_email |
| Scope | scope :active, -> { where(active: true) } |
| Query | User.where(active: true).order(created_at: :desc) |
Model Definition
class User < ApplicationRecord
# Constants
ROLES = %w[admin user guest].freeze
# Associations
has_many :posts, dependent: :destroy
has_many :comments
belongs_to :organization, optional: true
# Validations
validates :email, presence: true, uniqueness: true
validates :name, presence: true, length: { minimum: 2 }
validates :role, inclusion: { in: ROLES }
# Callbacks
before_save :normalize_email
after_create :send_welcome_email
# Scopes
scope :active, -> { where(active: true) }
scope :recent, -> { order(created_at: :desc) }
# Class methods
def self.search(query)
where("name ILIKE ?", "%#{query}%")
end
# Instance methods
def full_name
"#{first_name} #{last_name}"
end
private
def normalize_email
self.email = email.downcase.strip
end
end
Migrations
Creating Tables
class CreateUsers < ActiveRecord::Migration[7.0]
def change
create_table :users do |t|
t.string :name, null: false
t.string :email, null: false
t.boolean :active, default: true
t.integer :role, default: 0
t.references :organization, foreign_key: true
t.timestamps
end
add_index :users, :email, unique: true
end
end
Modifying Tables
class AddFieldsToUsers < ActiveRecord::Migration[7.0]
def change
add_column :users, :bio, :text
add_column :users, :avatar_url, :string
add_reference :users, :manager, foreign_key: { to_table: :users }
change_column_null :users, :email, false
change_column_default :users, :active, from: nil, to: true
end
end
Validations
class User < ApplicationRecord
# Presence
validates :email, presence: true
# Uniqueness
validates :email, uniqueness: { case_sensitive: false }
validates :username, uniqueness: { scope: :organization_id }
# Format
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :phone, format: { with: /\A\d{10}\z/ }
# Length
validates :name, length: { minimum: 2, maximum: 50 }
validates :bio, length: { maximum: 500 }
# Numericality
validates :age, numericality: { greater_than: 0, less_than: 150 }
# Inclusion/Exclusion
validates :role, inclusion: { in: ROLES }
validates :username, exclusion: { in: %w[admin root] }
# Custom validation
validate :email_domain_allowed
private
def email_domain_allowed
return if email.blank?
domain = email.split('@').last
unless %w[example.com company.com].include?(domain)
errors.add(:email, "must be from an allowed domain")
end
end
end
Associations
# One-to-Many
class Author < ApplicationRecord
has_many :books, dependent: :destroy
has_many :published_books, -> { where(published: true) }, class_name: 'Book'
end
class Book < ApplicationRecord
belongs_to :author
end
# Many-to-Many (has_and_belongs_to_many)
class Student < ApplicationRecord
has_and_belongs_to_many :courses
end
class Course < ApplicationRecord
has_and_belongs_to_many :students
end
# Many-to-Many (has_many :through)
class Student < ApplicationRecord
has_many :enrollments
has_many :courses, through: :enrollments
end
class Enrollment < ApplicationRecord
belongs_to :student
belongs_to :course
end
class Course < ApplicationRecord
has_many :enrollments
has_many :students, through: :enrollments
end
# One-to-One
class User < ApplicationRecord
has_one :profile, dependent: :destroy
end
class Profile < ApplicationRecord
belongs_to :user
end
# Polymorphic
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end
class Post < ApplicationRecord
has_many :comments, as: :commentable
end
class Photo < ApplicationRecord
has_many :comments, as: :commentable
end
Callbacks
class User < ApplicationRecord
# Order of execution:
before_validation :normalize_data
after_validation :log_validation_errors
before_save :encrypt_password
around_save :log_save_time
after_save :clear_cache
before_create :set_default_role
after_create :send_welcome_email
before_update :check_changes
after_update :notify_changes
before_destroy :check_dependencies
after_destroy :cleanup_files
private
def normalize_data
self.email = email.downcase if email.present?
end
def around_save
start_time = Time.current
yield
Rails.logger.info "Save took #{Time.current - start_time}s"
end
end
Scopes and Queries
class Post < ApplicationRecord
# Scopes
scope :published, -> { where(published: true) }
scope :recent, -> { order(created_at: :desc) }
scope :by_author, ->(author_id) { where(author_id: author_id) }
scope :created_between, ->(start_date, end_date) {
where(created_at: start_date..end_date)
}
# Chaining scopes
# Post.published.recent.limit(10)
end
# Query methods
Post.where(published: true)
Post.where("views > ?", 100)
Post.where(author_id: [1, 2, 3])
Post.where.not(category: 'draft')
# Ordering
Post.order(created_at: :desc)
Post.order(views: :desc, created_at: :asc)
# Limiting
Post.limit(10)
Post.offset(20).limit(10)
# Joins
Post.joins(:author)
Post.joins(:author, :comments)
Post.left_joins(:comments)
# Includes (eager loading)
Post.includes(:author, :comments)
Post.includes(author: :profile)
# Selecting specific fields
Post.select(:id, :title, :created_at)
Post.pluck(:title)
Post.pluck(:id, :title)
# Aggregations
Post.count
Post.average(:views)
Post.maximum(:views)
Post.minimum(:views)
Post.sum(:views)
# Group
Post.group(:category).count
Post.group(:author_id).average(:views)
Enums
class Post < ApplicationRecord
enum status: {
draft: 0,
published: 1,
archived: 2
}
# Or with prefix/suffix
enum visibility: {
public: 0,
private: 1
}, _prefix: :visibility
# Usage:
# post.draft!
# post.published?
# Post.published
# post.visibility_public!
end
Model Concerns
# app/models/concerns/taggable.rb
module Taggable
extend ActiveSupport::Concern
included do
has_many :taggings, as: :taggable
has_many :tags, through: :taggings
end
class_methods do
def tagged_with(tag_name)
joins(:tags).where(tags: { name: tag_name })
end
end
def tag_list
tags.pluck(:name).join(', ')
end
end
# Usage in model
class Post < ApplicationRecord
include Taggable
end
Best Practices
- Fat models, skinny controllers - Business logic belongs in models
- Use scopes for common queries
- Validate at database level with constraints when possible
- Use indexes for frequently queried columns
- Eager load associations to avoid N+1 queries
- Use concerns to share behavior across models
- Keep callbacks simple - avoid complex logic
- Use transactions for multi-step operations
- Avoid callbacks for cross-cutting concerns - use service objects instead
Common Pitfalls
- N+1 queries: Use
includes,preload, oreager_load - Callback hell: Keep callbacks simple, use service objects for complex logic
- Mass assignment vulnerabilities: Use strong parameters in controllers
- Missing indexes: Add indexes for foreign keys and frequently queried columns
- Ignoring database constraints: Add NOT NULL, unique constraints in migrations
References
Weekly Installs
3
Repository
shoebtamboli/ra…e_skillsGitHub Stars
4
First Seen
Mar 1, 2026
Security Audits
Installed on
opencode3
gemini-cli3
github-copilot3
codex3
amp3
cline3