Skip
Arish's avatar

30. Polymorphic Associations


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
8end

With 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
11end

Setting 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
14end

Models

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
15end

Using 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.count

Tags

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
23end

Addresses

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'
22end

Querying 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
19end

Best 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!