Rails Associations: A Complete Guide
Rails Associations: A Complete Guide
Rails associations define relationships between models. Understanding them is critical for building well-structured applications.
One-to-Many (has_many / belongs_to)
The most common association. One model “has many” instances of another.
# app/models/user.rb
class User < ApplicationRecord
has_many :posts, dependent: :destroy
end
# app/models/post.rb
class Post < ApplicationRecord
belongs_to :user
end
Usage
user = User.find(1)
user.posts # Get all posts
user.posts.create(title: 'New Post') # Create new post
user.posts.count # Count posts
post.user # Get associated user
Key Options
dependent: :destroy- Delete associated records when parent is deleteddependent: :nullify- Set foreign key to NULL (soft delete)dependent: :delete_all- Delete without running callbacksthrough: :association- For many-to-many relationships
Many-to-Many (has_many through)
Models are related through a join table.
# app/models/student.rb
class Student < ApplicationRecord
has_many :enrollments, dependent: :destroy
has_many :courses, through: :enrollments
end
# app/models/course.rb
class Course < ApplicationRecord
has_many :enrollments, dependent: :destroy
has_many :students, through: :enrollments
end
# app/models/enrollment.rb
class Enrollment < ApplicationRecord
belongs_to :student
belongs_to :course
end
Database Migration
create_table :enrollments do |t|
t.references :student, foreign_key: true
t.references :course, foreign_key: true
t.timestamps
end
Usage
student.courses # All courses for student
course.students # All students in course
student.enrollments # Join records (useful for extra data)
student.courses << course # Add course to student
One-to-One (has_one / belongs_to)
One model owns exactly one other model.
# app/models/user.rb
class User < ApplicationRecord
has_one :profile, dependent: :destroy
end
# app/models/profile.rb
class Profile < ApplicationRecord
belongs_to :user
end
Usage
user.profile # Get profile
user.profile.destroy # Destroy profile
profile.user # Get user
Polymorphic Associations
One model belongs to multiple different models.
# app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end
# app/models/post.rb
class Post < ApplicationRecord
has_many :comments, as: :commentable
end
# app/models/photo.rb
class Photo < ApplicationRecord
has_many :comments, as: :commentable
end
Database Migration
create_table :comments do |t|
t.text :body
t.references :commentable, polymorphic: true
t.timestamps
end
Usage
post.comments # Comments on post
photo.comments # Comments on photo
comment.commentable # Returns either Post or Photo
comment.commentable_type # String: "Post" or "Photo"
Common Pitfalls & Solutions
N+1 Query Problem
# BAD - Multiple queries
users = User.all
users.each { |user| puts user.posts.count } # One query per user!
# GOOD - Single query with joins
users = User.includes(:posts)
users.each { |user| puts user.posts.count } # No additional queries
Circular Dependencies
# BAD
class User < ApplicationRecord
has_many :posts
end
class Post < ApplicationRecord
belongs_to :user
has_many :users # Circular!
end
# GOOD - Use different association name
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
end
Foreign Key Constraints
# Always add indices and constraints
create_table :posts do |t|
t.references :user, foreign_key: true, null: false
end
# Equivalent to:
create_table :posts do |t|
t.integer :user_id, null: false
t.index :user_id
t.foreign_key :users
end
Best Practices
- Always specify dependent option - Prevent orphaned records
- Use includes/joins - Eliminate N+1 queries
- Add uniqueness constraints - Prevent duplicates
- Use class_name for clarity - When association name differs from model name
- Test associations - Easy to break, easy to test
Advanced: Scopes with Associations
class User < ApplicationRecord
has_many :posts
has_many :published_posts, -> { where(published: true) }, class_name: 'Post'
has_many :recent_posts, -> { order(created_at: :desc).limit(5) }, class_name: 'Post'
end
# Usage
user.published_posts # Only published
user.recent_posts # Last 5, ordered by recency
Conclusion
Rails associations are powerful once you understand them. Always remember:
- Use
includesfor performance - Specify
dependentbehavior - Keep associations simple and semantic
- Test edge cases thoroughly