117

Folks,

Want to make sure I understand this correctly. And please disregard the case for inheritance here (SentientBeing), trying to instead focus on polymorphic models in has_many :through relationships. That said, consider the following...

class Widget < ActiveRecord::Base
  has_many :widget_groupings

  has_many :people, :through => :widget_groupings, :source => :person, :conditions => "widget_groupings.grouper_type = 'Person'"
  has_many :aliens, :through => :widget_groupings, :source => :alien, :conditions => "video_groupings.grouper_type = 'Alien'"
end

class Person < ActiveRecord::Base
  has_many :widget_groupings, :as => grouper
  has_many :widgets, :through => :widget_groupings
end

class Alien < ActiveRecord::Base
  has_many :widget_groupings, :as => grouper
  has_many :widgets, :through => :widget_groupings  
end

class WidgetGrouping < ActiveRecord::Base
  belongs_to :widget
  belongs_to :grouper, :polymorphic => true
end

In a perfect world, I'd like to, given a Widget and a Person, do something like:

widget.people << my_person

However, when I do this, I've noticed the 'type' of the 'grouper' is always null in widget_groupings. However, if I to something like the following:

widget.widget_groupings << WidgetGrouping.new({:widget => self, :person => my_person}) 

Then all works as I would have normally expected. I don't think I've ever seen this occur with non polymorphic associations and just wanted to know if this was something specific to this use case or if I'm potentially staring at a bug.

Thanks for any help!

Ryan Bigg
  • 106,965
  • 23
  • 235
  • 261
Cory
  • 2,538
  • 2
  • 18
  • 19

4 Answers4

163

There is a known issue with Rails 3.1.1 that breaks this functionality. If you are having this problem first try upgrading, it's been fixed in 3.1.2

You're so close. The problem is you're misusing the :source option. :source should points to the polymorphic belongs_to relationship. Then all you need to do is specify :source_type for the relationship you're trying to define.

This fix to the Widget model should allow you do exactly what you're looking for.

class Widget < ActiveRecord::Base
  has_many :widget_groupings

  has_many :people, :through => :widget_groupings, :source => :grouper, :source_type => 'Person'
  has_many :aliens, :through => :widget_groupings, :source => :grouper, :source_type => 'Alien'
end
EmFi
  • 23,435
  • 3
  • 57
  • 68
  • Oh my god that is so painfully obvious I cannot believe I glazed right over it. Thanks EmFi! – Cory Nov 06 '09 at 15:58
  • No problem, I think I agonized for about a day over how to do this the first time I encountered it. Didn't help that it was one of the first things I tried to do in Rails that didn't involve following a tutorial/book. – EmFi Nov 06 '09 at 17:28
  • 1
    As scotkf points out, there is a regression in ActiveRecord 3.1.1 that blocks this behaviour. Upgrading to 3.1.2 will allow this solution to work. – EmFi Dec 05 '11 at 09:11
  • is it true that I can change source fields to just :as => :grouper – Shtirlic Jan 20 '12 at 17:08
  • 6
    Same thing as what @Shtirlic mentioned. Is there a way to not specify source_type, so you have a mixed results set? If anyone solved this, would love to know how. – Damon Aw Oct 22 '12 at 10:57
  • I keep getting "Unknown primary key for table 'jointable' in model 'Jointable'"... same code as above. – Donato Azevedo Feb 27 '14 at 01:51
  • @DonatoAzevedo: Does your migration define a primary key for that join table? – EmFi Feb 27 '14 at 20:56
  • Thank you so much for this! – jeffdill2 Jan 05 '16 at 01:13
  • 3
    Still works as of Rails 4.2.0. However, is there any way to accomplish this these days without source_type and two separate associations? – Emeka Feb 27 '16 at 15:27
3

As mentioned above, this doesn't work with rails 3.1.1 due to a bug on :source, but it's fixed in Rails 3.1.2

scottkf
  • 185
  • 3
  • 9
0

For example: locations have many trains, copters, trucks, ships. And trains, copters, trucks, ships have many locations.

Different locations can have similar transports(store in moveable polymorphic columns).

#db/migrations/create_moveable_locations.rb

class CreateMoveableLocations < ActiveRecord::Migration
  def change
    create_table :moveable_locations do |t|
      t.references :moveable, polymorphic: true
      t.references :location

      t.timestamps
    end
  end
end

#app/models/moveable_location.rb

class MoveableLocation < ActiveRecord::Base
  belongs_to :moveable, polymorphic: true
  belongs_to :location
end

#app/models/location.rb

class Location < ActiveRecord::Base
  has_many :moveable_locations, dependent: :destroy

  has_many :trains, through: :moveable_locations, source: :moveable, source_type: 'Train'
  has_many :copters, through: :moveable_locations, source: :moveable, source_type: 'Copter'
  has_many :trucks, through: :moveable_locations, source: :moveable, source_type: 'Truck'
  has_many :ships, through: :moveable_locations, source: :moveable, source_type: 'Ship'
end

#app/models/train.rb

class Train < ActiveRecord::Base
  has_many :moveable_locations, as: :moveable, dependent: :destroy
  has_many :locations, through: :moveable_locations
end

#app/models/copter.rb

class Copter < ActiveRecord::Base
  has_many :moveable_locations, as: :moveable, dependent: :destroy
  has_many :locations, through: :moveable_locations
end

#app/models/truck.rb

class Truck < ActiveRecord::Base
  has_many :moveable_locations, as: :moveable, dependent: :destroy
  has_many :locations, through: :moveable_locations
end

#app/models/ship.rb

class Ship < ActiveRecord::Base
  has_many :moveable_locations, as: :moveable, dependent: :destroy
  has_many :locations, through: :moveable_locations
end

Using:

train1 = Train.create(title: 'Train1')
train2 = Train.create(title: 'Train2')
location1 = Location.create(title: 'Location1', train_ids: [train1.id, train2.id])
location2 = Location.create(title: 'Location2', trains: [train1, train2])
location3 = Location.create(title: 'Location3')
location3.train_ids << [train1., train2.id]
location4 = Location.create(title: 'Location34')
location4.trains << [train1, train2]
copter1 = Copter.create(title: 'Copter1', location_ids: [location1.id, location2.id]
copter2 = Copter.create(title: 'Copter1')
copter2.location_ids << [location1.id, location2.id, location3.id]
shilovk
  • 11,718
  • 17
  • 75
  • 74
-4

has many :through and polymorphic don't work together. If you try to access them directly, it should throw an error. If i am not mistaken, you have to hand write widget.people and the push routine.

I don't think it is a bug, it is just something which hasn't been implemented yet. I would imagine we see it in the feature, because everyone has a case in which they could use it.

cgr
  • 1,093
  • 1
  • 8
  • 14
  • 6
    They do work together. For example: has_many :subscriptions, :as => :subscribable has_many :subscribers, :through => :subscriptions, :source => :user – ScottJ Nov 06 '09 at 20:46
  • I will throw up an example of my failing code as a separate post in the near future :) It would save me alot of headache to figure out how to bypass that error. – cgr Nov 07 '09 at 01:35