Active Record is Rails' Object-Relational Mapping (ORM) layer. It connects Ruby objects to database tables, providing an elegant API for creating, reading, updating, and deleting data without writing SQL.
This skill inherits all available tools. When active, it can use any tool Claude has access to.
examples/active-record-patterns.rbreferences/associations.mdreferences/migrations.mdreferences/query-optimization.mdActive Record is Rails' Object-Relational Mapping (ORM) layer. It connects Ruby objects to database tables, providing an elegant API for creating, reading, updating, and deleting data without writing SQL.
Active Record embodies Rails philosophy:
Master Active Record and you master data in Rails applications.
A model represents a table and provides domain logic:
# app/models/product.rb
class Product < ApplicationRecord
# Table: products
# Primary key: id
# Attributes: name, price, description, created_at, updated_at
end
Rails infers:
products (pluralized)idcreated_at, updated_atNo configuration needed—just convention.
| Element | Convention | Example |
|---|---|---|
| Model | Singular, CamelCase | Product, LineItem |
| Table | Plural, snake_case | products, line_items |
| Foreign key | model_id | user_id, category_id |
| Join table | Alphabetical models | orders_products |
| Primary key | id | Auto-generated integer |
Irregular pluralizations work automatically:
Person → peopleChild → childrenOctopus → octopiRails' inflector handles English pluralization rules.
Special column names have automatic behavior:
id: Primary key (auto-generated)created_at: Set when record createdupdated_at: Updated when record savedlock_version: Optimistic locking countertype: Single Table Inheritance discriminator{association}_id: Foreign key for associations{association}_type: Polymorphic association typeMigrations are Ruby scripts that modify database schema.
# Generate migration
rails generate migration CreateProducts name:string price:decimal
# Generate model (includes migration)
rails generate model Product name:string price:decimal
Generates:
# db/migrate/20240115100000_create_products.rb
class CreateProducts < ActiveRecord::Migration[8.0]
def change
create_table :products do |t|
t.string :name
t.decimal :price, precision: 10, scale: 2
t.timestamps
end
end
end
rails db:migrate # Run pending migrations
rails db:rollback # Undo last migration
rails db:migrate:status # Show migration status
rails db:migrate VERSION=20240115100000 # Migrate to specific version
Creating tables:
create_table :products do |t|
t.string :name, null: false
t.text :description
t.decimal :price, precision: 10, scale: 2
t.integer :quantity, default: 0
t.boolean :available, default: true
t.references :category, foreign_key: true
t.timestamps
end
Modifying tables:
change_table :products do |t|
t.rename :description, :details
t.change :price, :decimal, precision: 12, scale: 2
t.remove :quantity
t.string :sku
t.index :sku, unique: true
end
Adding columns:
add_column :products, :featured, :boolean, default: false
add_index :products, :name
add_reference :products, :supplier, foreign_key: true
Removing columns:
remove_column :products, :quantity
remove_index :products, :sku
remove_reference :products, :supplier
See references/migrations.md for comprehensive migration patterns.
Associations define relationships between models.
Declares a one-to-one or many-to-one relationship:
class Product < ApplicationRecord
belongs_to :category
# Expects: category_id column in products table
# Provides: product.category
end
Declares a one-to-many relationship:
class Category < ApplicationRecord
has_many :products
# Expects: category_id column in products table
# Provides: category.products
end
Declares a one-to-one relationship:
class User < ApplicationRecord
has_one :profile
# Expects: user_id column in profiles table
# Provides: user.profile
end
Many-to-many with join model:
class Order < ApplicationRecord
has_many :line_items
has_many :products, through: :line_items
end
class LineItem < ApplicationRecord
belongs_to :order
belongs_to :product
end
class Product < ApplicationRecord
has_many :line_items
has_many :orders, through: :line_items
end
Many-to-many without join model:
class Product < ApplicationRecord
has_and_belongs_to_many :tags
# Expects: products_tags join table (no id, no timestamps)
end
class Tag < ApplicationRecord
has_and_belongs_to_many :products
end
One model belongs to multiple model types:
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
# Expects: commentable_type and commentable_id columns
end
class Post < ApplicationRecord
has_many :comments, as: :commentable
end
class Product < ApplicationRecord
has_many :comments, as: :commentable
end
# Usage:
post.comments.create(body: "Great post!")
product.comments.create(body: "Love this product!")
See references/associations.md for advanced association patterns.
Active Record provides a rich query interface.
# Find by primary key
Product.find(1)
Product.find([1, 2, 3])
# Find by attributes
Product.find_by(name: "Widget")
Product.find_by!(name: "Widget") # Raises if not found
# First, last, all
Product.first
Product.last
Product.all
# Simple conditions
Product.where(available: true)
Product.where("price < ?", 10)
Product.where("price BETWEEN ? AND ?", 10, 50)
# Hash conditions
Product.where(category_id: [1, 2, 3])
Product.where.not(category_id: 1)
# Ranges
Product.where(created_at: 1.week.ago..Time.now)
# Pattern matching
Product.where("name LIKE ?", "%widget%")
Product.order(created_at: :desc)
Product.order(price: :asc, name: :asc)
Product.limit(10)
Product.offset(20).limit(10) # Pagination
Product.select(:id, :name, :price)
Product.select("id, name, UPPER(name) as uppercase_name")
# Inner join
Product.joins(:category)
Product.joins(:category, :tags)
# Left outer join
Product.left_outer_joins(:reviews)
# With conditions
Product.joins(:category).where(categories: { name: "Electronics" })
Problem (N+1 queries):
products = Product.all
products.each do |product|
puts product.category.name # Fires query for EACH product!
end
# Fires: 1 query for products + N queries for categories = N+1 queries
Solution (eager loading):
products = Product.includes(:category).all
products.each do |product|
puts product.category.name # Uses preloaded data
end
# Fires: 1 query for products + 1 query for categories = 2 queries total
Methods:
includes: Preload associations (two queries)eager_load: Preload with LEFT OUTER JOIN (one query)preload: Always uses separate queriesReusable query fragments:
class Product < ApplicationRecord
scope :available, -> { where(available: true) }
scope :cheap, -> { where("price < ?", 10) }
scope :expensive, -> { where("price > ?", 100) }
scope :in_category, ->(category) { where(category: category) }
end
# Usage:
Product.available.cheap
Product.expensive.in_category("Electronics")
Build complex queries incrementally:
products = Product.all
products = products.where(available: true) if params[:available]
products = products.where(category: params[:category]) if params[:category]
products = products.where("price < ?", params[:max_price]) if params[:max_price]
products = products.order(params[:sort] || :created_at)
products = products.page(params[:page])
products # Execute query when enumerated
Ensure data integrity before saving.
class Product < ApplicationRecord
validates :name, presence: true
validates :price, numericality: { greater_than: 0 }
validates :sku, uniqueness: true
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :description, length: { minimum: 10, maximum: 500 }
validates :category, presence: true
validates :terms, acceptance: true
end
validates :coupon_code, presence: true, if: :coupon_used?
validates :shipping_address, presence: true, unless: :pickup?
validate :price_must_be_reasonable
private
def price_must_be_reasonable
if price.present? && price > 10000
errors.add(:price, "is unreasonably high")
end
end
# Inline validation
product.valid? # => false
product.errors.full_messages # => ["Name can't be blank", "Price must be greater than 0"]
# Save with validation
product.save # => false (doesn't save if invalid)
product.save! # => raises ActiveRecord::RecordInvalid
# Skip validation (dangerous!)
product.save(validate: false)
Run code at specific points in an object's lifecycle.
class Product < ApplicationRecord
before_validation :normalize_name
after_validation :log_errors
before_save :calculate_discount
after_save :clear_cache
before_create :generate_sku
after_create :notify_team
before_update :track_price_changes
after_update :reindex_search
before_destroy :check_orders
after_destroy :cleanup_images
after_commit :sync_to_external_system
private
def normalize_name
self.name = name.strip.titleize if name.present?
end
def generate_sku
self.sku = SecureRandom.hex(8).upcase
end
def check_orders
throw :abort if orders.exists?
end
end
before_validationafter_validationbefore_savebefore_create / before_updateafter_create / after_updateafter_saveafter_commit / after_rollbackproduct.update_columns(price: 9.99) # Skips callbacks and validations
product.update_attribute(:price, 9.99) # Skips validations only
product.increment!(:view_count) # Skips validations, runs callbacks
class Vehicle < ApplicationRecord
# type column required
end
class Car < Vehicle
end
class Truck < Vehicle
end
# Queries:
Car.all # WHERE type = 'Car'
Vehicle.all # All vehicles
class Order < ApplicationRecord
enum status: [:pending, :processing, :shipped, :delivered, :cancelled]
end
order = Order.create!(status: :pending)
order.pending? # => true
order.processing! # Updates status to processing
order.processing? # => true
Order.pending # WHERE status = 0
Order.not_pending # WHERE status != 0
class BookOrder < ApplicationRecord
self.primary_key = [:book_id, :order_id]
belongs_to :book
belongs_to :order
end
# Find by composite key
BookOrder.find([book_id, order_id])
# JSON columns
add_column :products, :metadata, :jsonb, default: {}
product.metadata = { color: "red", size: "large" }
Product.where("metadata->>'color' = ?", "red")
# Arrays
add_column :products, :tags, :string, array: true, default: []
product.tags = ["electronics", "sale"]
Product.where("? = ANY(tags)", "electronics")
# Full-text search
Product.where("to_tsvector('english', name) @@ to_tsquery(?)", "widget")
# Case-insensitive queries (default)
Product.where("name = ?", "Widget") # Matches "widget", "WIDGET"
# JSON columns (MySQL 5.7+)
add_column :products, :settings, :json
explain to optimizeFor deeper exploration:
references/migrations.md: Complete migration guide with patternsreferences/associations.md: Advanced association techniquesreferences/query-optimization.md: Performance tuning and N+1 preventionFor code examples:
examples/active-record-patterns.rb: Common Active Record patternsActive Record provides:
Master Active Record, and you master data in Rails.