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

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

Related

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

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.

Many-to-many association with multiple self-joins in ActiveRecord

I am trying to implement multiple relations between records of the same model via self-joins (based on #Shtééf's answer). I have the following models
create_table :relations, force: true do |t|
t.references :employee_a
t.string :rel_type
t.references :employee_b
end
class Relation < ActiveRecord::Base
belongs_to :employee_a, :class_name => 'Employee'
belongs_to :employee_b, :class_name => 'Employee'
end
class Employee < ActiveRecord::Base
has_many :relations, foreign_key: 'employee_a_id'
has_many :reverse_relations, class_name: 'Relation', foreign_key: 'employee_b_id'
has_many :subordinates, through: :relations, source: 'employee_b', conditions: {'relations.rel_type' => 'manager of'}
has_many :managers, through: :reverse_relations, source: 'employee_a', conditions: {'relations.rel_type' => 'manager of'}
end
With this setup I can successfully access the lists of subordinates and managers for each record. However, I have difficulties to create relations in the following way
e = Employee.create
e.subordinates.create
e.subordinates #=> []
e.managers.create
e.managers #=> []
The problem is that it does not set type of relations, so I have to write
e = Employee.create
s = Employee.create
e.relations.create employee_b: s, rel_type: 'manager of'
e.subordinates #=> [#<Employee id:...>]
Am I doing something wrong?
You can use before_add and before_remove callback on the has_many association :
class Employee < ActiveRecord::Base
has_many :relations, foreign_key: 'employee_a_id'
has_many :reverse_relations, class_name: 'Relation', foreign_key: 'employee_b_id'
has_many :subordinates,
through: :relations,
source: 'employee_b',
conditions: {'relations.rel_type' => 'manager of'}
:before_add => Proc.new { |employe,subordinate| employe.relations.create(employe_b: subordinate, rel_type: 'manager of') },
:before_remove => Proc.new { |employe,subordinate| employe.relations.where(employe_b: subordinate, rel_type: 'manager of').first.destroy }
has_many :managers,
through: :reverse_relations,
source: 'employee_a',
conditions: {'relations.rel_type' => 'manager of'}
:before_add => Proc.new { |employe,manager| employe.reverse_relations.create(employe_a: manager, rel_type: 'manager of') },
:before_remove => Proc.new { |employe,manager| employe.reverse_relations.where(employe_b: subordinate, rel_type: 'manager of').first.destroy }
This should works and make you able to use employe.managers.create
You may want to use build instread of create in the callback
Also you can read this question about this solution
In order to create a multiple many-to-many self-join association, I would recommend that it might make more sense to have multiple tables to manage the connection. That way it's very clear from a data standpoint as to exactly what is going on, and it's also clear from a logic standpoint. So something along these lines:
create_table :manage_relation do |t|
t.references :employee_id
t.references :manager_id
end
create_table :subordinate_relation do |t|
t.references :employee_id
t.references :subordinate_id
end
class Employee < ActiveRecord::Base
has_many :subordinates,
:through => :subordinate_relation,
:class_name => "Employee",
:foreign_key => "subordinate_id"
has_many :managers,
:through => :manage_relation,
:class_name => "Employee",
:foreign_key => "manager_id"
belongs_to :employee,
:class_name => "Employee"
end
This way it doesn't get any more convoluted than necessary from a coding standpoint, and you can access it using the standard collections and it will appropriately set up your connections for you without you having to manage them. So, both of these collections should work..
employee.managers
employee.subordinates
And you could not have to manage any other variables. Make sense? It adds a table, but improves clarity.
I would redo your models as follows:
class ManagerRelation < ActiveRecord::Base
belongs_to :manager, :class_name => 'Employee'
belongs_to :subordinate, :class_name => 'Employee'
end
class Employee < ActiveRecord::Base
has_many :manager_relations, :class_name => "ManagerRelation",
:foreign_key => :subordinate_id
has_many :subordinate_relations, :class_name => "ManagerRelation",
:foreign_key => :manager_id
has_many :managers, :source => :manager,
:through => :manager_relations
has_many :subordinates, :source => :subordinate,
:through => :subordinate_relations
end
Now you can do the following:
employee.managers
employee.subordinates
employee.managers << employee2
employee.subordinates << employee3
Note: It is usually a sign for one to leave the company when they are made to report to two managers :-)
Given the presented relation
create_table :relations, force: true do |t|
t.references :employee_a
t.string :rel_type
t.references :employee_b
end
class Employee < ActiveRecord::Base
has_many :subordinate_relations, :class_name => "Relation", :conditions => {:rel_type => 'manager of'}, :foreign_key => :employee_a
has_many :subordinates, :through => :subordinate_relations, :source => :subordinate, :foreign_key => :employee_b
has_many :manager_relations, :class_name => "Relation", :conditions => {:rel_type => 'manager of'}, :foreign_key => :employee_b
has_many :managers, :through => :manager_relations, :source => :manager, :foreign_key => :employee_a
end
class Relation < ActiveRecord::Base
belongs_to :manager, :class_name => "Employee", :foreign_key => :employee_a
belongs_to :subordinate, :class_name => "Employee", :foreign_key => :employee_b
end
e = Employee.create
e.subordinates.create #Employee ...
e.subordinates #[<Employee ...]
e2 = Employee.create
e2.managers.create #Employee
e2.managers #[<Employee ...]
Although the solution works - I'm a bit confused by tying the associations with "rel_type". In this case - I'd say the rel_type is redundant and the relation should be mapped as follows:
create_table :relations do |t|
t.reference :manager
t.reference :subordinate
end
In such case, the association mapping should be a tad simpler.

Rails model references question

class CreateMatches < ActiveRecord::Migration
def self.up
create_table :matches do |t|
t.integer :result_home
t.integer :result_away
t.references :clan, :as => :clan_home
t.references :clan, :as => :clan_away
t.references :league
t.timestamps
end
end
def self.down
drop_table :matches
end
end
I think code clears everything, I need to reference result_home to one clan and result_away to another.
What is the best way to do so? I could create has_and_belongs_to_many but i think it's not good way in this case.
This looks like a join association call it Match, and
class Clan < ActiveRecord::Base
has_many :home_matches, :class_name => 'Match', :foreign_key => :clan_home
has_many :away_matches, :class_name => 'Match', :foreign_key => :clan_away
has_many :opponents_at_home, :through => :home_matches, :source => :clan
has_many :opponents_away, :through => :away_matches, :source => :clan
end
class Match < ActiveRecord::Base
belongs_to :clan_home, :class_name => 'Clan'
belongs_to :clan_away, :class_name => 'Clan'
end
This is a little beyond my personal experience and I'm not 100% clear on the interpretation of the documentation for :source (check http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html). However, I think this will be along the right lines. YMMV.
Comments and improvements are welcome!

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