I have two models, meetings and attendees which share a habtm relationship. I also have a User model to which a meeting can belong (as a meeting organizer).
class Meeting < ActiveRecord::Base
belongs_to :organizer, :class_name => User, :foreign_key => "organizer_id"
has_and_belongs_to_many :attendees, :class_name => User, :association_foreign_key => "attendee_id"
end
class User < ActiveRecord::Base
has_and_belongs_to_many :meetings, :class_name => Meeting, :association_foreign_key => "meeting_id"
end
and then I have the relationship table..
create_table "attendees_meetings", :id => false, :force => true do |t|
t.integer "attendee_id"
t.integer "meeting_id"
end
When I create a new meeting, and then reference the attendees as meeting.attendees, I get an error. Also the same thing with organizer, meeting.organizer throws an error. Have I not setup the relationships properly?
m = Meeting.create(:subject => "Test", :location => "Neverland", :body => "A test", :organizer_id => 8)
m.organizer
NoMethodError: undefined method `match' for #<Class:0x00000103d8cf08>
The same with attendees (though I have not defined any at the moment but shouldn't be throwing an error)
1.9.2-p318 :014 > m.attendees
(Object doesn't support #inspect)
=>
The class_name option on the has_and_belongs_to_many should be a string which is the name of the class. You have passed the class object itself. So, for example,
has_and_belongs_to_many :attendees,
:class_name => "User",
:association_foreign_key => "attendee_id"
I think you may also need to add a :foreign_key => 'attendee_id' on the has_and_belongs_to_many declaration in your User model and can remove the :association_foreign_key option because it is the default. Actually you can probably lose the :class_name option too as that is also the default. So:
has_and_belongs_to_many :meetings,
:foreign_key => "attendee_id"
Related
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
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
Those are my models:
class Person < ActiveRecord::Base
has_many :honors, :dependent => :destroy, :foreign_key => "honored_id"
has_many :honor_creators, :through => :honors, :source => :person, :foreign_key => "person_id"
class Group
has_many :honorss,:foreign_key => "group_id"
class Honor
belongs_to :person, :class_name => 'Person', :foreign_key => "person_id"
belongs_to :honored, :class_name => 'Person', :foreign_key => "honored_id"
belongs_to :group, :class_name => 'Group', :foreign_key => "group_id"
Since the honors are shown at the person#show page, here is my controller:
def show
...
#honors = #person.honors.paginate(:page => params[:page], :per_page => Honor.per_page)
end
And my view:
<% unless #honors.empty? %>
<% #honors.each do |ho| %>
My question is: using the ho I get all the attributes from the honor, but I want to get the creater of the honor and the group that it belongs. How can I do that?
Thanks!
You can do that by accessing
ho.person
or
ho.group
Note:
belongs_to :group, :class_name => 'Group', :foreign_key => "group_id"
and
belongs_to :group
will do the same thing because Rails will assume that class and foreign key when you give it :group.
To get ho.honor_creator to work (If I'm assuming correctly), you'll want:
class Person < Model
has_many :honors, :foreign_key => :creator_id
end
class Honor < Model
belongs_to :creator, :class_name => "Person"
end
That way, you can set honor.creator_id = params[:user_id] and then access the Person with ho.creator.
In other words, I'd redo your models like this:
class Person < ActiveRecord::Base
belongs_to :honor, :foreign_key => :recipient_id
has_many :honors, :foreign_key => :creator_id
class Group
has_many :honors
class Honor
has_many :recipients, :class_name => 'Person' # recipient_id is assumed
belongs_to :creator, :class_name => 'Person' # creator_id is assumed
belongs_to :group # group_id is assumed
I renamed some columns to hopefully make better sense, and this is done off the top of my head, so I'll look at some reference to ensure my accuracy.
This will give you access to honor.creator and honor.group.
<% #honors.each do |honor| %>
Honor Creator: <%= honor.creator.name %>
Honor Group: <%= honor.group.name %>
Honor Recipients: <% honor.recipients.each {|recipient| puts recipient} %>
<% end %>
Might have messed up the recipient association, but I'll go look.
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.
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.