Rails: Class inheritance and complex polymorphic has_many :through association - ruby-on-rails

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

Related

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?

Ruby on Rails: polymorphic many-to-many design?

I'm having troubles getting a polymorphic many-to-many model working in ruby/rails. The model has three tables that need to be joined, Infection, Drug, and Symptom:
create_table "diseases" do |t|
t.string "name"
end
create_table "drugs" do |t|
t.string "name"
end
create_table "symptoms" do |t|
t.string "name"
end
create_table "to_symptoms" do |t|
t.integer "symptom_id"
t.integer "symptomatic_id"
t.string "symptomatic_type"
end
Where symptoms is linked to both infections and drugs. The tricky part is that the relationship of a symptom to a drug can be either as a side effect or as a contraindication. The way I tried to do this was:
class ToSymptom < ActiveRecord::Base
belongs_to :symptomatic, :polymorphic => true
belongs_to :symptom
end
class Drug < ActiveRecord::Base
has_many :to_symptom, :as => :symptomatic
has_many :contraindications, :class_name => "Symptom",
:through => :to_symptom, :source => :symptomatic,
:source_type => 'Contraindication'
has_many :side_effects, :class_name => "Symptom",
:through => :to_symptom, :source => :symptomatic,
:source_type => 'SideEffect'
end
class Symptom < ActiveRecord::Base
has_many :to_symptom
has_many :diseases, :through => :to_symptom, :source => :symptomatic,
:source_type => 'Disease'
has_many :contraindicated_drugs, :class_name => "Drug",
:through => :to_symptom, :source => :symptomatic,
:source_type => 'Contraindication'
has_many :caused_by, :class_name => "Drug", :through => :to_symptom,
:source => :symptomatic, :source_type => 'SideEffect'
end
class Disease < ActiveRecord::Base
has_many :to_symptom, :as => :symptomatic
has_many :symptoms, :through => :to_symptom
end
The Disease <-> Symptom relationship seems to be working the way I'd expect, but the relationships between Drug and Symptom aren't doing what I'd expect. The relationship in the direction of symptoms-> drugs seems to be working, but the reverse direction generates some weird SQL. If I try something like:
d = Drug.first
d.contraindications
I'll get the following SQL:
SELECT
`symptoms`.*
FROM `symptoms`
INNER JOIN `to_symptoms` ON `symptoms`.`id` = `to_symptoms`.`symptomatic_id`
WHERE `to_symptoms`.`symptomatic_id` = 2
AND `to_symptoms`.`symptomatic_type` = 'Drug'
AND `to_symptoms`.`symptomatic_type` = 'Contraindication'
The to.symptoms.symptomatic_type = drug shouldn't be in there, and the join in on the wrong field of to_symptoms (symptomatic_id vs. symptom_id. I've tried a ton of different combinations, but I can't seem to get this one to work. Is what I'm trying to do even possible in RoR?
It seems that this isn't very widely advertised, but it apparently doesn't work in Rails... (polymorphic mas_many :through) (at least not without insane hacks). I'll try to find some supporting links

ActiveRecord, has_many :through, Polymorphic Associations with STI

In ActiveRecord, has_many :through, and Polymorphic Associations, the OP's example requests ignoring the possible superclass of Alien and Person (SentientBeing). This is where my question lies.
class Widget < ActiveRecord::Base
has_many :widget_groupings
has_many :people, :through => :widget_groupings, :source => :person, :source_type => 'Person'
has_many :aliens, :through => :widget_groupings, :source => :alien, :source_type => 'Alien'
end
SentientBeing < ActiveRecord::Base
has_many :widget_groupings, :as => grouper
has_many :widgets, :through => :widget_groupings
end
class Person < SentientBeing
end
class Alien < SentientBeing
end
In this modified example the grouper_type value for Alien and Person are now both stored by Rails as SentientBeing (Rails seeks out the base class for this grouper_type value).
What is the proper way to modify the has_many's in Widget to filter by type in such a case? I want to be able to do Widget.find(n).people and Widget.find(n).aliens, but currently both of these methods (.people and .aliens) return empty set [] because grouper_type is always SentientBeing.
Have you tried the simplest thing - adding :conditions to the has_many :throughs?
In other words, something like this (in widget.rb):
has_many :people, :through => :widget_groupings, :conditions => { :type => 'Person' }, :source => :grouper, :source_type => 'SentientBeing'
has_many :aliens, :through => :widget_groupings, :conditions => { :type => 'Alien' }, :source => :grouper, :source_type => 'SentientBeing'
JamesDS is correct that a join is needed - but it's not written out here, since the has_many :through association is already doing it.

Rails - Model Doubt

Given the fact that I have models like this:
class Person
has_many :owned_groups, :class_name => "Group", :foreign_key => :owner_id
has_many :owned_group_memberships, :through => :owned_groups,
:source => :group_memberships
has_many :group_memberships, :foreign_key => "member_id"
has_many :groups, :through => :group_memberships
end
class GroupMembership
belongs_to :member, :class_name => 'Person'
belongs_to :group
end
class Group
belongs_to :owner, :class_name => "Person"
has_many :group_memberships
has_many :members, :through => :group_memberships
end
How can I access the members a group has? Always I do #group.members or things like that, gives me an error saying that the relation can't be found in the model.
Thanks in advance.
##EDIT##
The error I'm getting is: Could not find the association "group_memberships" in model Group
I do a similar thing on a site I'm working on but the associations are a little different to how you're doing it but maybe it'll help. I think you need to use the has_and_belongs_to_many association to join up your many-to-many's.
In my database I have Users, Members and UsersMembers
You don't need to create a UsersMembers model (GroupMembership in your case) but you do need a database table to link the two.
#Migration
create_table :bands_users, :id => false, :force => true do |t|
t.integer :band_id, :null => false
t.integer :user_id, :null => false
end
#Models
class Band < ActiveRecord::Base
has_and_belongs_to_many :members, :class_name => 'User'
end
class User < ActiveRecord::Base
has_and_belongs_to_many :bands
end
From this I can now call #band.members or #user.bands
You may need to specify :class_name => 'Person' in your has_many :members statement.

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

Resources