Created

Embed URL

HTTPS clone URL

SSH clone URL

You can clone with HTTPS or SSH.

Download Gist

Reverse polymorphic associations in Rails

View description.markdown

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.

View description.markdown
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
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

Thanks to @bryanrite for edits!

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

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.

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

royletron,

Did you add the following line to your Picture class?

attr_accessible :description

So how do you call @article.media?

ayrton commented

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

What about validations?

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

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?

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.