Polymorphic Associations
Polymorphic associations allow a model to belong to more than one other model using a single association. This is useful when multiple models share similar relationships.
Why Polymorphic Associations?
Imagine you want comments on both articles and photos. Without polymorphism:
ruby
1# You'd need separate tables and models
2class ArticleComment < ApplicationRecord
3 belongs_to :article
4end
5
6class PhotoComment < ApplicationRecord
7 belongs_to :photo
8endWith polymorphism, you need just one Comment model:
ruby
1class Comment < ApplicationRecord
2 belongs_to :commentable, polymorphic: true
3end
4
5class Article < ApplicationRecord
6 has_many :comments, as: :commentable
7end
8
9class Photo < ApplicationRecord
10 has_many :comments, as: :commentable
11endSetting Up Polymorphic Associations
Migration
ruby
1class CreateComments < ActiveRecord::Migration[7.1]
2 def change
3 create_table :comments do |t|
4 t.text :body
5 t.references :commentable, polymorphic: true, null: false
6 t.timestamps
7 end
8
9 # This creates:
10 # - commentable_id (integer)
11 # - commentable_type (string)
12 # - index on both columns
13 end
14endModels
ruby
1class Comment < ApplicationRecord
2 belongs_to :commentable, polymorphic: true
3end
4
5class Article < ApplicationRecord
6 has_many :comments, as: :commentable, dependent: :destroy
7end
8
9class Photo < ApplicationRecord
10 has_many :comments, as: :commentable, dependent: :destroy
11end
12
13class Video < ApplicationRecord
14 has_many :comments, as: :commentable, dependent: :destroy
15endUsing Polymorphic Associations
ruby
1# Create comments on different models
2article = Article.create(title: "My Article")
3photo = Photo.create(url: "photo.jpg")
4
5# Add comments
6article.comments.create(body: "Great article!")
7photo.comments.create(body: "Nice photo!")
8
9# Access comments
10article.comments # Comments for this article
11photo.comments # Comments for this photo
12
13# Access the parent from comment
14comment = Comment.first
15comment.commentable # Returns Article or Photo
16comment.commentable_type # => "Article" or "Photo"
17comment.commentable_id # => 1
18
19# Check the type
20comment.commentable.is_a?(Article) # => true
21comment.commentable_type == "Article"Real-World Examples
Attachments
ruby
1class Attachment < ApplicationRecord
2 belongs_to :attachable, polymorphic: true
3
4 has_one_attached :file
5end
6
7class Project < ApplicationRecord
8 has_many :attachments, as: :attachable, dependent: :destroy
9end
10
11class Task < ApplicationRecord
12 has_many :attachments, as: :attachable, dependent: :destroy
13end
14
15class Message < ApplicationRecord
16 has_many :attachments, as: :attachable, dependent: :destroy
17end
18
19# Usage
20project.attachments.create(file: uploaded_file)
21task.attachments.create(file: uploaded_file)Activity Feed / Events
ruby
1class Event < ApplicationRecord
2 belongs_to :eventable, polymorphic: true
3 belongs_to :user
4
5 # action could be: 'created', 'updated', 'deleted', 'liked', etc.
6end
7
8class Article < ApplicationRecord
9 has_many :events, as: :eventable
10
11 after_create { events.create(user: Current.user, action: 'created') }
12 after_update { events.create(user: Current.user, action: 'updated') }
13end
14
15class Comment < ApplicationRecord
16 has_many :events, as: :eventable
17
18 after_create { events.create(user: Current.user, action: 'created') }
19end
20
21# Get recent activity
22Event.includes(:eventable, :user)
23 .order(created_at: :desc)
24 .limit(20)Likes / Favorites
ruby
1class Like < ApplicationRecord
2 belongs_to :likeable, polymorphic: true
3 belongs_to :user
4
5 validates :user_id, uniqueness: { scope: [:likeable_type, :likeable_id] }
6end
7
8class Article < ApplicationRecord
9 has_many :likes, as: :likeable, dependent: :destroy
10 has_many :liking_users, through: :likes, source: :user
11
12 def liked_by?(user)
13 likes.exists?(user: user)
14 end
15end
16
17class Comment < ApplicationRecord
18 has_many :likes, as: :likeable, dependent: :destroy
19 has_many :liking_users, through: :likes, source: :user
20end
21
22# Usage
23article.likes.create(user: current_user)
24article.liked_by?(current_user)
25article.likes.countTags
ruby
1class Tagging < ApplicationRecord
2 belongs_to :taggable, polymorphic: true
3 belongs_to :tag
4end
5
6class Tag < ApplicationRecord
7 has_many :taggings, dependent: :destroy
8
9 # Get all items with this tag
10 def items
11 taggings.map(&:taggable)
12 end
13end
14
15class Article < ApplicationRecord
16 has_many :taggings, as: :taggable, dependent: :destroy
17 has_many :tags, through: :taggings
18end
19
20class Photo < ApplicationRecord
21 has_many :taggings, as: :taggable, dependent: :destroy
22 has_many :tags, through: :taggings
23endAddresses
ruby
1class Address < ApplicationRecord
2 belongs_to :addressable, polymorphic: true
3
4 validates :street, :city, :zip, presence: true
5end
6
7class User < ApplicationRecord
8 has_many :addresses, as: :addressable, dependent: :destroy
9 has_one :primary_address, -> { where(primary: true) },
10 as: :addressable, class_name: 'Address'
11end
12
13class Company < ApplicationRecord
14 has_many :addresses, as: :addressable, dependent: :destroy
15 has_one :headquarters, -> { where(type: 'headquarters') },
16 as: :addressable, class_name: 'Address'
17end
18
19class Order < ApplicationRecord
20 has_one :shipping_address, as: :addressable, class_name: 'Address'
21 has_one :billing_address, as: :addressable, class_name: 'Address'
22endQuerying Polymorphic Associations
ruby
1# Find all comments for a specific type
2Comment.where(commentable_type: "Article")
3
4# Find comments for a specific article
5Comment.where(commentable: article)
6Comment.where(commentable_type: "Article", commentable_id: article.id)
7
8# Eager loading (note: can be tricky)
9Comment.includes(:commentable).where(commentable_type: "Article")
10
11# Find items with specific comments
12Article.joins(:comments).where(comments: { approved: true })STI vs Polymorphic
Sometimes Single Table Inheritance is a better choice:
ruby
1# Polymorphic: when the child model (Comment) can belong to multiple parents
2# STI: when you have variations of the same model
3
4# STI example
5class Notification < ApplicationRecord
6 # type column distinguishes subclasses
7end
8
9class EmailNotification < Notification
10 def send!
11 # Send email
12 end
13end
14
15class SmsNotification < Notification
16 def send!
17 # Send SMS
18 end
19endBest Practices
ruby
1# 1. Always add index on polymorphic columns
2add_index :comments, [:commentable_type, :commentable_id]
3
4# 2. Consider using concerns for shared behavior
5module Commentable
6 extend ActiveSupport::Concern
7
8 included do
9 has_many :comments, as: :commentable, dependent: :destroy
10 end
11
12 def recent_comments
13 comments.order(created_at: :desc).limit(5)
14 end
15end
16
17class Article < ApplicationRecord
18 include Commentable
19end
20
21class Photo < ApplicationRecord
22 include Commentable
23end
24
25# 3. Be careful with eager loading
26# This works but loads all types separately
27Comment.includes(:commentable)
28
29# More efficient for specific type
30Comment.where(commentable_type: 'Article')
31 .includes(:commentable)Polymorphic associations are powerful for building flexible, DRY models!
