Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bi-directional polymorphic join model in Rails?

I'm working on a multi-site CMS that has a notion of cross-publication among sites. Several types of content (Articles, Events, Bios, etc) can be associated with many Sites and Sites can have many pieces of content. The many-to-many association between content pieces and sites must also support a couple common attributes for each content item associated -- the notion of site origination (is this the original site upon which the content appeared?) as well as a notion of "primary" and "secondary" content status for a given piece of content on a given associated site.

My idea has been to create a polymorphic join model called ContentAssociation, but I'm having trouble getting the polymorphic associations to behave as I expect them to, and I'm wondering if perhaps I'm going about this all wrong.

Here's my setup for the join table and the models:

create_table "content_associations", :force => true do |t|
  t.string   "associable_type"
  t.integer  "associable_id"
  t.integer  "site_id"
  t.boolean  "primary_eligible"
  t.boolean  "secondary_eligible"
  t.boolean  "originating_site"
  t.datetime "created_at"
  t.datetime "updated_at"
end

class ContentAssociation < ActiveRecord::Base
  belongs_to :site
  belongs_to :associable, :polymorphic => true
  belongs_to :primary_site, :class_name => "Site", :foreign_key => "site_id" 
  belongs_to :secondary_site, :class_name => "Site", :foreign_key => "site_id"
  belongs_to :originating_site, :class_name => "Site", :foreign_key => "site_id"
end

class Site < ActiveRecord::Base
  has_many :content_associations, :dependent => :destroy 
  has_many :articles, :through => :content_associations, :source => :associable, :source_type => "Article"
  has_many :events, :through => :content_associations, :source => :associable, :source_type => "Event"

  has_many :primary_articles, :through => :content_associations, 
                              :source => :associable, 
                              :source_type => "Article", 
                              :conditions => ["content_associations.primary_eligible = ?" true]

  has_many :originating_articles, :through => :content_associations, 
                                  :source => :associable, 
                                  :source_type => "Article", 
                                  :conditions => ["content_associations.originating_site = ?" true]

  has_many :secondary_articles, :through => :content_associations, 
                                :source => :associable, 
                                :source_type => "Article", 
                                :conditions => ["content_associations.secondary_eligible = ?" true]
end

class Article < ActiveRecord::Base
  has_many :content_associations, :as => :associable, :dependent => :destroy
  has_one :originating_site, :through => :content_associations, 
                             :source => :associable, 
                             :conditions => ["content_associations.originating_site = ?" true]

  has_many :primary_sites, :through => :content_associations, 
                           :source => :associable
                           :conditions => ["content_associations.primary_eligible = ?" true]

  has_many :secondary_sites, :through => :content_associations, 
                             :source => :associable
                             :conditions => ["content_associations.secondary_eligible = ?" true]                         
end

I've tried a lot of variations of the above association declarations, but no matter what I do, I can't seem to get the behavior I want

@site = Site.find(2)
@article = Article.find(23)
@article.originating_site = @site
@site.originating_articles #=>[@article]

or this

@site.primary_articles << @article
@article.primary_sites #=> [@site]

Is Rails' built-in polymorphism the wrong mechanism to use to affect these connections between Sites and their various pieces of content? It seems like it would be useful because of the fact that I need to connect multiple different models to a single common model in a many-to-many way, but I've had a hard time finding any examples using it in this manner.

Perhaps part of the complexity is that I need the association in both directions -- i.e. to see all the Sites that a given Article is associated with and see all of the Articles associated with a given Site. I've heard of the plugin has_many_polymorphs, and it looks like it might solve my problems. But I'm trying to use Rails 3 here and not sure that it's supported yet.

Any help is greatly appreciated -- even if it just sheds more light on my imperfect understanding of the uses of polymorphism in this context.

thanks in advance!

like image 735
trevrosen Avatar asked Feb 13 '26 12:02

trevrosen


2 Answers

If you need the associations to be more extensible than STI would allow, you can try writing your own collection helpers that do extra type-introspection.

Any time you define a relationship with belongs_to, has_many or has_one etc. you can also define helper functions related to that collection:

class Article < ActiveRecord::Base
  has_many :associations, :as => :associable, :dependent => :destroy
  has_many :sites, :through => :article_associations

  scope :originating_site, lambda { joins(:article_associations).where('content_associations.originating_site' => true).first }
  scope :primary_sites, lambda { joins(:article_associations).where('content_associations.primary_eligable' => true) }
  scope :secondary_sites, lambda { joins(:article_associations).where('content_associations.secondary_eligable' => true) }
end

class Site < ActiveRecord::Base
  has_many :content_associations, :as => :associable, :dependent => :destroy do
    def articles
      collect(&:associable).collect { |a| a.is_a? Article }
    end
  end
end

class ContentAssociation < ActiveRecord::Base
  belongs_to :site
  belongs_to :associable, :polymorphic => true
  belongs_to :primary_site, :class_name => "Site", :foreign_key => "site_id"
  belongs_to :secondary_site, :class_name => "Site", :foreign_key => "site_id"
  belongs_to :originating_site, :class_name => "Site", :foreign_key => "site_id"
end

You could move those function defs elsewhere if you need them to be more DRY:

module Content
  class Procs
    cattr_accessor :associations
    @@associations = lambda do
      def articles
        collect(&:associable).collect { |a| a.is_a? Article }
      end

      def events
        collect(&:associable).collect { |e| e.is_a? Event }
      end

      def bios
        collect(&:associable).collect { |b| b.is_a? Bio }
      end
    end
  end
end


class Site < ActiveRecord::Base
  has_many :content_associations, :as => :associable, :dependent => :destroy, &Content::Procs.associations
end

And since articles, events & bios in this example are all doing the same thing, we can DRY this even more:

module Content
  class Procs
    cattr_accessor :associations
    @@associations = lambda do
      %w(articles events bios).each do |type_name|
        type = eval type_name.singularize.classify
        define_method type_name do
          collect(&:associable).collect { |a| a.is_a? type }
        end
      end
    end
  end
end

And now it's starting to become more like a generic plugin, rather than application-specific code. Which is good, because you can reuse it easily.

like image 154
Adam Lassek Avatar answered Feb 16 '26 02:02

Adam Lassek


Just a shot, but have you looked at polymorphic has_many :through => relationships? There's a few useful blog posts about - try http://blog.hasmanythrough.com/2006/4/3/polymorphic-through and http://www.inter-sections.net/2007/09/25/polymorphic-has_many-through-join-model/ (there was also a question here). Hope some of that helps a bit, good luck!

like image 41
Budgie Avatar answered Feb 16 '26 00:02

Budgie