Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Reverse polymorphic associations in Rails

Polymorphic Associations reversed

It's pretty easy to do polymorphic associations in Rails: A Picture can belong to either a BlogPost or an Article. But what if you need the relationship the other way around? A Picture, a Text and a Video can belong to an Article, and that article can find all media by calling @article.media

This example shows how to create an ArticleElement join model that handles the polymorphic relationship. To add fields that are common to all polymorphic models, add fields to the join model.

class Article < ActiveRecord::Base
has_many :article_elements
has_many :pictures, :through => :article_elements, :source => :element, :source_type => 'Picture'
has_many :videos, :through => :article_elements, :source => :element, :source_type => 'Video'
end
class Picture < ActiveRecord::Base
has_one :article_element, :as =>:element
has_one :article, :through => :article_elements
end
class Video < ActiveRecord::Base
has_one :article_element, :as =>:element
has_one :article, :through => :article_elements
end
class ArticleElement < ActiveRecord::Base
belongs_to :article
belongs_to :element, :polymorphic => true
end
t = Article.new
t.article_elements # []
p = Picture.new
t.article_elements.create(:element => p)
t.article_elements # [<ArticleElement id: 1, article_id: 1, element_id: 1, element_type: "Picture", created_at: "2011-09-26 18:26:45", updated_at: "2011-09-26 18:26:45">]
t.pictures # [#<Picture id: 1, created_at: "2011-09-26 18:26:45", updated_at: "2011-09-26 18:26:45">]
Owner

runemadsen commented Dec 14, 2011

Thanks to @bryanrite for edits!

Malsu commented Aug 2, 2012

For some reason this was letting me view Articles through Pictures, but not Pictures through Articles. Had to change the following:

has_many :pictures, :through => :article_elements, :source => :elements, :source_type => 'Picture'
has_many :videos, :through => :article_elements, :source => :elements, :source_type => 'Video'

to

has_many :pictures, :through => :article_elements, :source => :element, :source_type => 'Picture'
has_many :videos, :through => :article_elements, :source => :element, :source_type => 'Video'

in my equivalent model.

Thank you very much!

Owner

runemadsen commented Aug 2, 2012

Fixed! Thanks @Malsu

Hey guys, thanks for this info.

I'm doing something similar just now where I have a Card (a user story for a kanban wall) which has attachments and notes - I want them to all to be available as a generic 'timeline item' class.

Any chance you could dump the schema for these classes so I can see what the column name conventions are?

Cheers!

Superb work, thanks. When I run t.article_elements.create(:element => p) I'm getting "Can't mass assign protected attributes: element". Any ideas? As far as I can tell, all the attr_accessibles are in order.

jifka commented Jan 25, 2013

@rafaelmadeira
I had the same problem when I implemented it as-is. You need to add :element to the attr_accessible list on the ArticleElement class.

I seem to have a problem where if I add 'description' as an attribute to ArticleElement and then call:

p = Picture.new
p.description = "This is a description"

I get the error

undefined method `description=' for #<Picture:0x007fc1e7777828>

fromar commented Mar 8, 2013

royletron,

Did you add the following line to your Picture class?

attr_accessible :description

So how do you call @article.media?

ayrton commented Jun 26, 2013

@kbighorse he mentioned media in the description, yet in his example he named his class ArticleElement.
So call article.article_elements instead.

ronyv89 commented Jan 10, 2014

What about validations?

terkill commented Jan 25, 2014

I'm getting:
t = Article.new
t.article_elements # nil
p = Picture.new
t.article_elements.create(:element => p) #NoMethodError: undefined method `create' for nil:NilClass

t.article_elements.create(:element => p)
ActiveRecord::RecordNotSaved: You cannot call create unless the parent is saved

Here's an example that uses STI:

# STI parent
#
#  id           :integer          not null, primary key
#  type         :string(255)      not null
#  title        :string(255)      not null
#  content      :text
#
class Post < ActiveRecord::Base
  scope :articles, -> { where(type: "Article") }
  scope :pages, -> { where(type: "Page") }
end

# STI child (taggable)
#
#  Uses posts table.
#
class Article < Post
  has_many :taggings, as: :taggable, dependent: :destroy
  has_many :tags, through: :taggings
end

# STI child (taggable)
#
#  Uses posts table.
#
class Page < Post
  has_many :taggings, as: :taggable, dependent: :destroy
  has_many :tags, through: :taggings
end

# Independent model (taggable)
#
#  id                   :integer          not null, primary key
#  title                :string(255)      not null
#  url                  :string(255)      not null
#
class Video < ActiveRecord::Base
  has_many :taggings, as: :taggable, dependent: :destroy
  has_many :tags, through: :taggings
end

# Many-to-many polymorphic table
#
#  id            :integer          not null, primary key
#  tag_id        :integer          not null
#  taggable_id   :integer          not null
#  taggable_type :string(255)      not null
#
class Tagging < ActiveRecord::Base
  belongs_to :tag
  belongs_to :taggable, polymorphic: true
end

# Attach a tag to any model through taggings
#
#  id         :integer          not null, primary key
#  title      :string(255)      not null
#
class Tag < ActiveRecord::Base
  has_many :taggings, dependent: :destroy

  with_options through: :taggings, source: :taggable do |tag|
    tag.has_many :articles, source_type: "Post", class_name: "Article"
    tag.has_many :pages, source_type: "Post", class_name: "Page"
    tag.has_many :videos, source_type: "Video"
  end
end

Ajedi32 commented Aug 18, 2014

I'm confused. Why is the ArticleElement class required? Why can't Picture and Video just belong_to :article, as: :article_element, and Article has_many :article_elements, polymorphic: true?

rodamn commented Aug 29, 2015

@Ajedi32 Trying to figure out the answer to your question has helped me get a better understanding of Rails polymorphism. Here's my answer, it might help someone else who comes across this.

The way polymorphism in Rails works: there is one – and only one – class (per polymorphic relationship) that belongs_to other unrelated classes. These other classes can have either a has_one or a has_many relationship to the belonging model. More concretely, there is a) one class, Polymorph, with belongs_to :polymorphable, polymorphic: true, and b) one or more classes that has_one or has_many :polymorphs, as: polymorphable. Thus, Picture and Video cannot both have the polymorphic belongs_to. Further, the has_many association cannot be polymorphic.

Basically, with "belongs_to, polymorphable" Rails uses the polymorphable_id and polymorphable_type columns in the underlying table as a foreign_key and table selector, respectively. Normally, belongs to just indicates there is a foreign key, and Rails infers the table name based on belongs_to's argument. With polymorphism, it is switching the table based on the _type. That should provide further clarity on why two tables cannot belong_to the same :symbol.

Based on the needs of the data domain, it might be possible that ArticleElement is not necessary; i.e. Article belongs_to: :element, polymorphic: true. In this case though, ArticleElement is currently acting as a join table between multiple Elements (which happens to not actually be a table) and multiple Articles.

@runemadsen is this applicable to this scenario?

i have two models Projects and Tasks these model must be associated to schedule model which contains the dueDates and startDates

hbarudin commented Jul 13, 2016 edited

Commenting here just to see if anyone knows: What would the db migrations look like for this scenario? has_many :through has always hurt my brain a little, so I'm having trouble writing the migration by hand. I've been looking at this StackOverflow, but so far I can't get it to work 😢

Romain-C commented Mar 3, 2017

@hbarudin The polymorphic migration can be achieved as below :

create_table :article_elements do |t|
    ...
    t.timestamps null: false
    t.references :article, index: true
    t.references :element, polymorphic: true, index: true
  end

Polymorphic is a bad idea as there is no way to keep the database consistent due to no FK.

dorelly2 commented Jul 4, 2017 edited

In Picture change it to: has_one :article, :through => :article_element

dorelly2 commented Jul 4, 2017

What is interesting is inventing some kind of a form helper for the Article (to change a Picture to Video etc.)

WMahoney09 commented Sep 19, 2017 edited

I have the same question as @kbighorse
The issue I have with the answer given by @ayrton is that the result would be an array of ArticleElements, not an array of Pictures and Videos.
I would like to provide an ActiveRecord helper on my Article that reads like Article.first.media that does not bring back a bunch of joins, but reaches through the join and assembles a list of what is on the other side of that polymorphic table, i.e. a mix of Pictures and Videos that are polymorphically related to a given Article
I suppose this could be done by asking for Article.first.pictures and joining that manually with Article.first.videos

This post explains why it is at least tricky if not impossible to traverse a polymorphic :through relationship in the "other" direction

One option would be

class Article < ActiveRecord::Base
  has_many :article_elements
  has_many :pictures, :through => :article_elements, :source => :element, :source_type => 'Picture'
  has_many :videos, :through => :article_elements, :source => :element, :source_type => 'Video'

  def media
    self.article_elements.collect{ |el| el.element }
  end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment