Rails ActiveRecord Patterns
Model design, scopes, callbacks, concerns, N+1 prevention.
# CLAUDE.md — Rails ActiveRecord Patterns
## Migrations
- One migration per logical change. Never edit a migration after it's merged — write a new one.
- Reversible migrations by default. `change` is fine for simple operations; use `up`/`down` for complex ones.
- Add indexes when adding foreign keys: `t.references :user, foreign_key: true, index: true`.
- For large data backfills, use a separate `data:migrate` rake task — not the schema migration.
## Models
- Skinny associations, smart scopes:
```ruby
class Invoice < ApplicationRecord
belongs_to :customer
has_many :line_items, dependent: :destroy
scope :unpaid, -> { where(paid_at: nil) }
scope :recent, ->(n = 30) { where("created_at > ?", n.days.ago) }
end
```
- Always set `dependent:` on `has_many`. The default is silent orphans.
- Validations belong on the model. Don't validate in controllers or service objects.
## Callbacks
- Use sparingly. `before_validation` for normalization (downcase email). `after_create_commit` for events that should fire only after the DB is durable.
- Don't put cross-model side effects in callbacks. They run in spooky places (specs, console, batch imports). Use service objects.
- Skip callbacks intentionally with `update_columns` for one-off backfills — and document why.
## Queries
- `find` for primary key (raises if missing). `find_by` for other unique columns (returns nil).
- Avoid `find_by_<col>!` — `find_by(col: ...)!` is the modern equivalent.
- Eager-load with `includes(:assoc)`. Profile with `bullet` to catch N+1s.
- For complex reads, write a query object PORO. Don't bury 30-line scopes in the model.
## Concerns
- `app/models/concerns/` for reusable behavior. Name with the capability: `Sluggable`, `SoftDeletable`, `Auditable`.
- Concerns hold validations, scopes, and methods that always go together. They don't replace good model design.
## Counter caches & denormalization
- Counter caches: `belongs_to :post, counter_cache: true`. Adds `posts_count` increments — saves COUNT queries.
- Don't denormalize until you measure the cost. Caches drift.
## Soft delete
- If you must, add a `discarded_at:datetime` and use the `discard` gem.
- Default scope to non-discarded with care — it's surprising in batch operations.
- Most data shouldn't be soft-deleted. The "we might restore it later" is rarely real.
## Don't
- Don't use `update_attribute` (singular). It skips validations.
- Don't use `unscoped` to "see all rows" in production code. If a default scope hides too much, the scope is wrong.
- Don't write SQL via interpolation: `where("name = '#{params[:name]}'")` is injection. Use placeholders.
- Don't read all rows then filter in Ruby. Push the filter to SQL.
Other Ruby on Rails templates
Modern Rails Rules
Rails 7+ best practices: conventions, generators, and the Omakase stack.
Rails + Hotwire (Stimulus + Turbo)
Server-rendered frontends with Turbo Frames, Streams, and Stimulus.
Rails Testing with RSpec
RSpec setup, factories, system specs, request specs, and fast suites.
Rails Deployment (Kamal + Docker)
Deploy Rails with Kamal, Docker, and the Solid Queue/Cable stack.