Bi-directional polymorphic join model in Rails? - ruby-on-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!

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.

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!

In this case I don't think polymorphism is the right way to go, at least from what I understand of your system's design. Here's an example using STI. It's complicated, so forgive me if I'm missing something. I'm also not very strong on the new arel syntax, so can't guarantee this will function without tinkering.
class Article < ActiveRecord::Base
has_many :article_associations, :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, :dependent => :destroy
has_many :article_associations
has_many :articles, :through => :article_associations
end
class ContentAssociation < ActiveRecord::Base
belongs_to :site
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 ArticleAssociation < ContentAssociation
belongs_to :article
end
What I'm doing here is creating a base association model and a separate child association for each data type. So, if you need to access associations by type you'll have access to site.articles but you can also get a list of site.content_assocations with everything together.
The STI feature will need a type:string column to store the datatype. This will be handled automatically unless you're using the ContentAssociation model. Since ArticleAssociation is using article_id you'll also need to add that, and every other column the child models use.

Related

Model attribute contains collection of object

I have main model Page, which is container.
The page can have some to-do lists, notes, files and discussions. The idea is to have them in special order.
Page.last.container # [Todolist_obj, Note_obj, File_obj, Note_obj, Discussion_obj, File_obj, File_obj]
So I came to approach to use Mongodb
Or I also thought about using Postgres with hstore, but don't know will it help or not
Or maybe just any database and deserialize all objects when getting page, and serialize objects when saving
Or I can make superclass Item and inherit all containing objects from it using MTI and make Page has many relationship.
So I don't know which way is the best?
or perhaps there is a better way?
I have used acts_as_list for implementing sortable objects very successfully. Additionally, i would abstract the elements of a page into a separate model, here called PageElement.
I think there is no need to switch to a NoSQL database (although i have nothing against this approach). Here is a rough sketch of what i'm thinking:
class Page < ActiveRecord::Base
has_many :page_elements, :order => 'position'
has_many :todo_lists, :through => :page_elements, :source => :element, :source_type => 'TodoList'
has_many :notes, :through => :page_elements, :source => :element, :source_type => 'Note'
has_many :files, :through => :page_elements, :source => :element, :source_type => 'File'
has_many :discussions, :through => :page_elements, :source => :element, :source_type => 'Discussion'
end
class PageElement < ActiveRecord::Base
belongs_to :page
belongs_to :element, :polymorphic => true
acts_as_list :scope => :page
end
class TodoList < ActiveRecord::Base
has_one :page_element, :as => :element
has_one :page, :through => :page_elements
end
class Note < ActiveRecord::Base
has_one :page_element, :as => :element
has_one :page, :through => :page_elements
end
class File < ActiveRecord::Base
has_one :page_element, :as => :element
has_one :page, :through => :page_elements
end
class Discussion < ActiveRecord::Base
has_one :page_element, :as => :element
has_one :page, :through => :page_elements
end

Rails: delete polymorphic STI has_many through association fails

I have a model called User which has many "taxonomies" associated through a Classification model. One of these taxonomies is a model called Topic (inheriting from Taxonomy). My model User is also called a "classifiable".
EDIT: Added more models to clarify the problem
class User < ActiveRecord::Base
has_many :classifications, :as => :classifiable, :foreign_key => :classifiable_id
has_many :topics, :through => :classifications, :source => :taxonomy, :source_type => "Topic"
end
class Taxonomy < ActiveRecord::Base
end
class Topic < Taxonomy
has_many :classifications, :as => :taxonomy, :foreign_key => :taxonomy_id, :source_type => "Topic"
has_many :professionals, :through => :classifications, :source => :classifiable, :source_type => "User", :conditions => {:is_a_professional => true}
has_many :questions, :through => :classifications, :source => :classifiable, :source_type => "Question"
has_many :guides, :through => :classifications, :source => :classifiable, :source_type => "Guide"
end
class Classification < ActiveRecord::Base
attr_accessible :classifiable, :classifiable_id, :classifiable_type,
:taxonomy, :taxonomy_id, :taxonomy_type
belongs_to :classifiable, :polymorphic => true
belongs_to :taxonomy, :polymorphic => true
end
Everything works well except when I want to delete an association.
user = User.find(12) # any user
topic = user.topics.last # any of his topics
user.topics.delete(topic)
The SQL ActiveRecord runs is the following:
DELETE FROM "classifications" WHERE "classifications"."classifiable_id" = 12 AND "classifications"."classifiable_type" = 'User' AND "classifications"."taxonomy_id" = 34 AND "classifications"."taxonomy_type" = 'Taxonomy'
Clearly, the taxonomy_type is wrong, it should be 'Topic' and not 'Taxonomy'.
Since I am using polymorphic associations and STI, I had to config ActiveRecord as such:
ActiveRecord::Base.store_base_sti_class = false
However, it does not seem to trigger on collection.delete. Is this a bug with rails?

Rails: Class inheritance and complex polymorphic has_many :through association

The app I'm developing has 3 main models and many single table inheritance models:
Question
User
Professional
Representant
Taxonomy
Category
Topic
Profession
Locality
Region
Country
There are multiple kinds of users (User, Professional < User, Representant < User) which all inherit from the User class with single table inheritance.
There are multiple kinds of taxonomies (Category < Taxonomy, Topic < Taxonomy, Profession < Taxonomy, Locality < Taxonomy, Region < Taxonomy, Country < Taxonomy) which all inherit from the Taxonomy class with single table inheritance.
Questions, as well as professionals are also under taxonomies via many to many relationships (they can have many topics, many professions, many categories, etc...)
Now, I'm looking for a way to establish that many to many relationship between those polymorphic objects. I've tried the has_many :through solution and created a Classification class.
Migration file:
class CreateClassifications < ActiveRecord::Migration
def change
create_table :classifications, :id => false do |t|
t.references :classifiable, :null => false, :default => 0, :polymorphic => true
t.references :taxonomy, :null => false, :default => 0, :polymorphic => true
end
add_index :classifications, [:classifiable_id, :taxonomy_id]
add_index :classifications, [:taxonomy_id, :classifiable_id]
end
end
Model file:
class Classification < ActiveRecord::Base
attr_accessible :classifiable, :classifiable_id, :classifiable_type,
:taxonomy, :taxonomy_id, :taxonomy_type
belongs_to :classifiable, :polymorphic => true
belongs_to :taxonomy, :polymorphic => true
end
I've then added has_many :through associations for Questions, Professionals, and Taxonomies.
Taxonomy.rb
has_many :classifications, :as => :taxonomy, :foreign_key => :taxonomy_id
has_many :classifiables, :through => :classifications, :source => :classifiable
has_many :users, :through => :classifications, :source => :classifiable, :source_type => "User"
has_many :professionals, :through => :classifications, :source => :classifiable, :source_type => "Professional"
has_many :representants, :through => :classifications, :source => :classifiable, :source_type => "Representant"
has_many :questions, :through => :classifications, :source => :classifiable, :source_type => "Question"
has_many :guides, :through => :classifications, :source => :classifiable, :source_type => "Guide"
Question.rb
has_many :classifications, :as => :classifiable, :foreign_key => :classifiable_id, :dependent => :destroy
has_many :taxonomies, :through => :classifications, :source => :taxonomy
has_many :topics, :through => :classifications, :source => :taxonomy, :source_type => "Topic"
Professional.rb
has_many :classifications, :as => :classifiable, :foreign_key => :classifiable_id, :dependent => :destroy
has_many :taxonomies, :through => :classifications, :source => :taxonomy
has_many :topics, :through => :classifications, :source => :taxonomy, :source_type => "Topic"
has_many :professions, :through => :classifications, :source => :taxonomy, :source_type => "Profession"
Now, after setting up all this, things do not work very well...
I can't seem to assign taxonomies to Professionals or Questions (i.e. Question.create(:title => "Lorem Ipsum Dolor Sit Amet", :author => current_user, :topics => [list of topics,...]) works well except for topics which are not saved.)
Where clauses don't work as they should (i.e. Question.joins(:topics).where(:conditions => {:topics => {:id => [list of topics,...]}}) fails with a no such column: "Topics"."id" error.
Any help? Thanks!
UPDATE
I have installed the gem 'store_base_sti_class' as indicated. It had the desired effect on the Classification model.
#<Classification classifiable_id: 1, classifiable_type: "Professional", taxonomy_id: 17, taxonomy_type: "Topic">
However, when I query topics (Professional.find(1).topics), ActiveRecord is still looking for the class "User" instead of "Professional"...
SELECT "taxonomies".* FROM "taxonomies" INNER JOIN "classifications" ON "taxonomies"."id" = "classifications"."taxonomy_id" WHERE "taxonomies"."type" IN ('Topic') AND "classifications"."classifiable_id" = 1 AND "classifications"."classifiable_type" = 'User' AND "classifications"."taxonomy_type" = 'Topic'
Any idea how to fix it for both?
For question #2, the keys in the where clause should map to table names, not association names. So I think you would want:
Question.joins(:topics).where(Topic.table_name => {:id => [...]})
For question #1, it appears that when you set question.topics = [...], the Classification objects which Rails creates are being set with a taxonomy_type of "Taxonomy" (instead of "Topic"). That appears to be due to Rails' through_association.rb:51, which takes the base_class of the model being stored, instead of just the actual class name.
I was able to get around this with a before_validation callback on the Classification model. It seems to me that the alternative is a patch to the actual Rails associations code, to make this behavior configurable.
class Classification < ActiveRecord::Base
attr_accessible :classifiable, :classifiable_id, :classifiable_type,
:taxonomy, :taxonomy_id, :taxonomy_type
belongs_to :classifiable, polymorphic: true
belongs_to :taxonomy, polymorphic: true
before_validation :set_valid_types_on_polymorphic_associations
protected
def set_valid_types_on_polymorphic_associations
self.classifiable_type = classifiable.class.model_name if classifiable
self.taxonomy_type = taxonomy.class.model_name if taxonomy
end
end
UPDATE
There appears to be another Rails decision (in preloader/association.rb:113) to use the model.base_class.sti_name instead of the model.sti_name when setting scope for associations.
That gem should take care of this for you. See store_base_sti_class_for_3_1_and_above.rb:135 for how it wraps the has_many :as option. In my local environment, this works as expected:
$ bundle exec rails console
irb(main):001:0> topics = 3.times.map { Topic.create }
irb(main):002:0> p = Professional.new
irb(main):003:0> p.topics = topics
irb(main):004:0> p.save!
irb(main):005:0> exit
$ bundle exec rails console
irb(main):001:0> puts Professional.find(1).topics.to_sql
SELECT "taxonomies".* FROM "taxonomies" INNER JOIN "classifications" ON "taxonomies"."id" = "classifications"."taxonomy_id" WHERE "taxonomies"."type" IN ('Topic') AND "classifications"."classifiable_id" = 2 AND "classifications"."classifiable_type" = 'Professional' AND "classifications"."taxonomy_type" IN ('Topic')
irb(main):002:0> Professional.find(1).topics.count
=> 3

What would be a good way to model interchangable items in Rails 3?

Here is an example of the structure I'm trying to achieve. I'll just setup a silly example so it's easier to explain:
Let's say we have a Product that holds a bunch of different hammers. Each hammer has multiple attributes (weight, size, color, etc) but they all can hammer nails in. So in that respect they are interchangeable. However they are not interchangeable with other products, like a chainsaw or a sledgehammer.
So I want to be able to keep a list of all products that are interchangeable. So if one hammer is not available I could see ids all the other products that I can give to a customer instead.
Since I don't know how many interchangeable products each product can have ( let's say there are 5 different hammers, and 50 screwdrivers), I can't just create an interchangeable field to keep this info. i was thinking about enums, but they are more complicated to run reports on.
Here is what I have so far, but I'm not sure if this is the best solution (it's kind of late and my mind is starting to melt - this is a much simplified example if my self referential tables):
class Product < ActiveRecord::Base
has_many :interchangable_products, :dependent => :destroy
end
class InterchangableProduct < ActiveRecord::Base
belongs_to :product, :class => "Product", :foreign_key => :product_id
belongs_to :interchangable_with, :class_name => "InterchangableProduct", :foreign_key => :interchangable_with_id
has_many :interchangables, :class_name => "InterchangableProduct", :inverse_of => :interchangable_with, :foreign_key => :interchangable_with_id
validates :product_id, :presence => true, :uniqueness => [:scope => :interchangable_with_id]
end
Thank you
Thanks to a comment from Dave Newton I arrived to a solution that looks sort of like this:
class InterchangeableProductRelationship < ActiveRecord::Base
belongs_to :product, :class_name => "Product", :foreign_key => "product_id"
belongs_to :interchangeable_product, :class_name => "Product", :foreign_key => "interchangeable_product_id"
validates :product_id, :presence => true, :uniqueness => [:scope => :interchangeable_product_id]
end
class Product < ActiveRecord::Base
has_many :relations_to, :foreign_key => 'product_id', :class_name => "InterchangeableProductRelationship"
has_many :relations_from, :foreign_key => 'interchangeable_product_id', :class_name => "InterchangeableProductRelationship"
has_many :linked_to, :through => :relations_to, :source => :interchangeable_product
has_many :linked_from, :through => :relations_from, :source => :product
def interchanges_with
self.linked_to | self.linked_from
end
end
Hopefully this will be helpful to someone.

Polymorphic associations in Rails 3

I think I'm going crazy.
Let's say I have 3 models: Address, Warehouse, Category:
class Address < ActiveRecord::Base
belongs_to :category
belongs_to :addressable, :polymorphic => true
scope :billing_addresses , where(:categories => {:name => 'billing'}).joins(:category)
scope :shipping_addresses , where(:categories => {:name => 'shipping'}).joins(:category)
end
class Category < ActiveRecord::Base
has_many :addresses
has_many :subcategories, :class_name => "Category", :foreign_key => "category_id"
belongs_to :category, :class_name => "Category"
end
class Warehouse < ActiveRecord::Base
has_many :addresses, :as => :addressable
end
Address is polymorphic, because eventually I'll be using it to store addresses for clients, people, employees etc. Also each address can be of a certain type: billing, shipping, work, home, etc.
I'm trying to pull some information on a page.
#some_warehouse = Warehouse.first
Then in my view:
%b= #some_warehouse.name
%b= #some_warehouse.billing_address.address_line_1
Etc.
I end up doing a lookup for each line of information.
I tried to do things like
Warehouse.includes(:addresses).where(:name => "Ware1")
Warehouse.joins(:addresses).where(:name => "Ware1")
And various variations of that.
No matter what I don' I can't get rails to preload all the tables. What am I doing wrong?
Here are revised models, that do appropriate joins in sql and reduce number of quesries from 16 to 8, one for each piece of info, instead of multiples ones that also do lookup categories, etc.:
class Address < ActiveRecord::Base
belongs_to :category
belongs_to :addressable, :polymorphic => true
scope :billing_addresses , where(:categories => {:name => 'billing'}).includes(:category)
scope :shipping_addresses , where(:categories => {:name => 'shipping'}).includes(:category)
end
class Warehouse < ActiveRecord::Base
has_many :addresses, :as => :addressable, :include => :category, :dependent => :destroy
def billing_address
self.addresses.billing_addresses.first
end
def shipping_address
self.addresses.shipping_addresses.first
end
end
class Category < ActiveRecord::Base
has_many :addresses
has_many :subcategories, :class_name => "Category", :foreign_key => "category_id"
belongs_to :category, :class_name => "Category"
end
Sleep helps. Also not forgetting to reload console from time to time :-)
Maybe you want to use preload_associations?

Resources