rails-models
npx machina-cli add skill Shoebtamboli/rails_claude_skills/rails-models --openclawFiles (1)
SKILL.md
8.4 KB
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
Source
git clone https://github.com/Shoebtamboli/rails_claude_skills/blob/main/lib/generators/claude/skills_library/rails-models/SKILL.mdView on GitHub Overview
Rails Models (ActiveRecord) define how data is stored, validated, and related. This skill covers model definitions, migrations, validations, associations, and callbacks, with practical patterns shown in the SKILL.md examples.
How This Skill Works
Models inherit from ApplicationRecord and declare associations (has_many, belongs_to), validations (presence, format, uniqueness), and callbacks (before_save, after_create). Migrations create and modify tables, add indexes, and enforce constraints. Scopes and class methods provide reusable queries, like active or search patterns shown in the reference.
When to Use It
- Model a user-like entity with name, email, active flag, and role to demonstrate validations and associations.
- Create and evolve database tables with migrations (create_table, add_column, references) and maintain indexes.
- Enforce data integrity with validations (presence, uniqueness, format, length, numericality) and custom validators.
- Model relationships using has_many, belongs_to, and has_many :through or has_and_belongs_to_many, including dependent behavior.
- Apply callbacks (before_save, after_create) for side effects like email normalization or welcome messages.
Quick Start
- Step 1: rails g model User name:string email:string
- Step 2: Define validations, associations, and callbacks in app/models/user.rb
- Step 3: Run rails db:migrate and test with rails console
Best Practices
- Place presence, format, and uniqueness validations close to the fields they guard; leverage ROLES/SCOPE patterns where relevant.
- Add a unique index on critical fields (e.g., email) to complement validations and improve lookups.
- Use appropriate associations with dependent options (e.g., has_many :posts, dependent: :destroy) and through relationships for complexity.
- Encapsulate business logic in class methods or scopes rather than controllers or views.
- Apply callbacks judiciously and keep them fast and testable (e.g., normalize_email in before_save).
Example Use Cases
- User model with has_many :posts, validates :email, presence and uniqueness.
- Migration CreateUsers with name, email, active flag, role, organization reference and a unique index on email.
- Validation rules for email format and phone number, plus length restrictions on names and bios.
- Associations: Author has_many :books, Book belongs_to :author; and many-to-many examples with has_and_belongs_to_many or has_many :through.
- Callbacks: before_save :normalize_email and after_create :send_welcome_email as shown in the sample.
Frequently Asked Questions
Add this skill to your agents