14
votes

Using Ruby on Rails, how can I achieve a polymorphic has_many relationship where the owner is always of a known but the items in the association will be of some polymorphic (but homogenous) type, specified by a column in the owner? For example, suppose the Producer class has_many products but producer instances might actually have many Bicycles, or Popsicles, or Shoelaces. I can easily have each product class (Bicycle, Popsicle, etc.) have a belongs_to relationship to a Producer but given a producer instance how can I get the collection of products if they are of varying types (per producer instance)?

Rails polymorphic associations allow producers to belong to many products, but I need the relationship to be the other way around. For example:

class Bicycle < ActiveRecord::Base
  belongs_to :producer
end

class Popsicle < ActiveRecord::Base
  belongs_to :producer
end

class Producer < ActiveRecord::Base
  has_many :products, :polymorphic_column => :type # last part is made-up...
end

So my Producer table already has a "type" column which corresponds to some product class (e.g. Bicycle, Popsicle, etc.) but how can I get Rails to let me do something like:

>> bike_producer.products
#=> [Bicycle@123, Bicycle@456, ...]
>> popsicle_producer.products
#=> [Popsicle@321, Popsicle@654, ...]

Sorry if this is obvious or a common repeat; I'm having surprising difficulty achieving it easily.

5
Just as a note, I strongly recommend against using Factory as a model name, as factory_girl is a very commonly used extension used for generating models instead of fixtures, and this may be very confusing for people reading your code. - Jamie Wong
@jamie - thanks for the tip, I've switch terminology to "Producer", which hopefully won't be confused with any concurrency libraries =) - maerics
No problem. Also, I haven't found a good solution to this. As far as I know, the ability for an object to have polymorphic children does not yet exist in Rails. You may want to check out blog.hasmanythrough.com/2006/4/3/polymorphic-through for reference though. - Jamie Wong
Yeah, just read that article. I've hacked a simple instance method that uses a mapping of column value to class name and does a find_by_producer_id, which works fine, but I don't get any of the association convenience methods. I thought surely this was a solved problem but perhaps not... - maerics
What is the advantage of using polymorphic relationships over inheriting all of your different products from a base Product model? Single Table Inheritance seems like it would solve many of your problems. - nuclearsandwich

5 Answers

6
votes

You have to use STI on the producers, not on the products. This way you have different behavior for each type of producer, but in a single producers table.

(almost) No polymorphism at all!

class Product < ActiveRecord::Base
  # does not have a 'type' column, so there is no STI here,
  # it is like an abstract superclass.
  belongs_to :producer
end

class Bicycle < Product
end

class Popsicle < Product
end

class Producer < ActiveRecord::Base
  # it has a 'type' column so we have STI here!!
end

class BicycleProducer < Producer
  has_many :products, :class_name => "Bicycle", :inverse_of => :producer
end

class PopsicleProducer < Producer
  has_many :products, :class_name => "Popsicle", :inverse_of => :producer
end
2
votes

please take it on format

class Bicycle < ActiveRecord::Base 
  belongs_to :bicycle_obj,:polymorphic => true 
end 

class Popsicle < ActiveRecord::Base
  belongs_to :popsicle_obj , :polymorphic => true 
end 

class Producer < ActiveRecord::Base 
  has_many :bicycles , :as=>:bicycle_obj 
  has_many :popsicle , :as=>:popsicle_obj 
end 

Use this code. If you have any problem with it, please leave a comment.

1
votes

Here is the workaround I'm currently using. It doesn't provide any of the convenience methods (collection operations) that you get from real ActiveRecord::Associations, but it does provide a way to get the list of products for a given producer:

class Bicycle < ActiveRecord::Base
  belongs_to :producer
end

class Popsicle < ActiveRecord::Base
  belongs_to :producer
end

class Producer < ActiveRecord::Base
  PRODUCT_TYPE_MAPPING = {
    'bicycle' => Bicycle,
    'popsicle' => Popsicle
  }.freeze
  def products
    klass = PRODUCT_TYPE_MAPPING[self.type]
    klass ? klass.find_all_by_producer_id(self.id) : []
  end
end

Another downside is that I must maintain the mapping of type strings to type classes but that could be automated. However, this solution will suffice for my purposes.

0
votes

I find that polymorphic associations is under documented in Rails. There is a single table inheritance schema, which is what gets the most documentation, but if you are not using single table inheritance, then there is some missing information.

The belongs_to association can be enabled using the :polymorphic => true option. However, unless you are using single table inheritance, the has_many association does not work, because it would need to know the set of tables that could have a foreign key.

(From what I found), I think the clean solution is to have a table and model for the base class, and have the foreign key in the base table.

create_table "products", :force => true do |table|
    table.integer  "derived_product_id"
    table.string   "derived_product_type"
    table.integer  "producer_id"
  end

  class Product < ActiveRecord::Base
    belongs_to :producer
  end

  class Producer < ActiveRecord::Base
    has_many :products
  end

Then, for a Production object, producer, you should get the products with producer.products.derived_products.

I have not yet played with has_many through to condense the association to producer.derived_products, so I cannot comment on getting that to work.

-2
votes
class Note < ActiveRecord::Base

 belongs_to :note_obj, :polymorphic => true
 belongs_to :user


end


class Contact < ActiveRecord::Base

 belongs_to :contact_obj, :polymorphic => true
 belongs_to :phone_type 

end



class CarrierHq < ActiveRecord::Base


 has_many :contacts, :as => :contact_obj
 has_many :notes, :as => :note_obj


end