Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails model with two polymorphic has_many through: associations for object tagging

My schema has Articles and Journals that can be tagged with Tags. This requires a has_many through: association with a polymorphic relationship to my Tagging join table.

Okay, that's the easy and well documented part.

My problem is that Articles can have both primary-tags and sub-tags. The primary tags are what I'm most interested in, but my model also needs to keep track of these sub tags. Sub tags are simply labels describing the Article that are of lesser importance, but come from the same global pool of Tags. (in fact, one Article's primary-tag may be another's sub-tag).

Achieving this requires the Article model to have two associations to the Tagging model and two has_many through: associations to Tags (i.e. #tags & #sub-tags)

This is what I have so far, which while valid does not keep primary-tags and sub-tags separate.

class Article < ActiveRecord::Base
  has_many :taggings, as: :taggable
  has_many :tags, through: :taggings

  has_many :sub_taggings, as: :taggable, class_name: 'Tagging',
           source_type: 'article_sub'
  has_many :sub_tags, through: :sub_taggings, class_name: 'Tag', source: :tag
end

class Tagging < ActiveRecord::Base
  #  id            :integer
  #  taggable_id   :integer
  #  taggable_type :string(255)
  #  tag_id        :integer
  belongs_to :tag
  belongs_to :taggable, :polymorphic => true
end

class Tag < ActiveRecord::Base
  has_many :taggings
end

I know that somewhere in there I need to find the right combination of source and source_type but I can't work it out.

For completeness here is my article_spec.rb that I'm using to test this — currently failing on "the incorrect tags".

describe "referencing tags" do
  before do
    @article.tags << Tag.find_or_create_by_name("test")
    @article.tags << Tag.find_or_create_by_name("abd")
    @article.sub_tags << Tag.find_or_create_by_name("test2")
    @article.sub_tags << Tag.find_or_create_by_name("abd")
  end

  describe "the correct tags" do
    its(:tags) { should include Tag.find_by_name("test") }
    its(:tags) { should include Tag.find_by_name("abd") }
    its(:sub_tags) { should include Tag.find_by_name("abd") }
    its(:sub_tags) { should include Tag.find_by_name("test2") }
  end

  describe "the incorrect tags" do
    its(:tags) { should_not include Tag.find_by_name("test2") }
    its(:sub_tags) { should_not include Tag.find_by_name("test") }
  end
end

Thanks in advance for any help on achieving this. The main problem is I cannot work out how to tell Rails the source_type to use for the sub_tags association in Articles.

like image 584
djoll Avatar asked Apr 06 '13 10:04

djoll


1 Answers

Hmmm... Silence again...? What gives SO? Hello...? Bueller?

Never fear, here's the answer:

After looking into Single Table Inheritance (not the answer, but an interesting technique for other slightly-related problems), I stumbled across a SO question regarding multiple-references to a polymorphic association on the same model. (Thank you hakunin for your detailed answer, +1.)

Basically we need to explicitly define the contents of the taggable_type column in the Taggings table for the sub_taggings association, but not with source or source_type, instead with :conditions.

The Article model shown below now passes all the tests:

 class Article < ActiveRecord::Base
   has_many :taggings, as: :taggable
   has_many :tags, through: :taggings, uniq: true, dependent: :destroy

   has_many :sub_taggings, as: :taggable, class_name: 'Tagging',
             conditions: {taggable_type: 'article_sub_tag'},
             dependent: :destroy
   has_many :sub_tags, through: :sub_taggings, class_name: 'Tag',
             source: :tag, uniq: true
 end

UPDATE:

This is the correct Tag model that produces functional reverse polymorphic associations on Tags. The reverse association (ie. Tag.articles and Tag.sub_tagged_articles) passes tests.

 class Tag < ActiveRecord::Base
   has_many :articles, through: :taggings, source: :taggable,
             source_type: "Article"
   has_many :sub_tagged_articles, through: :taggings, source: :taggable,
             source_type: "Article_sub_tag", class_name: "Article"
 end

I have also extended and successfully tested the schema to allow tagging & sub_tagging of other models using the same Tag model and Tagging join table. Hope this helps someone.

like image 198
djoll Avatar answered Sep 30 '22 17:09

djoll