Skip to content

@runemadsen /description.markdown
Created

Embed URL

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
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">]
@runemadsen
Owner

Thanks to @bryanrite for edits!

@Malsu

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!

@runemadsen
Owner

Fixed! Thanks @Malsu

@stuliston

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!

@rafaelmadeira

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

@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.

@royletron

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

royletron,

Did you add the following line to your Picture class?

attr_accessible :description

@kbighorse

So how do you call @article.media?

@ayrton

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

@ronyv89

What about validations?

@terkill

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

@subzero125

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

@benlinton

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

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

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.