15

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.

8
  • 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. Commented Jul 9, 2010 at 2:04
  • @jamie - thanks for the tip, I've switch terminology to "Producer", which hopefully won't be confused with any concurrency libraries =) Commented Jul 9, 2010 at 2:15
  • 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. Commented Jul 9, 2010 at 2:28
  • 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... Commented Jul 9, 2010 at 2:31
  • 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. Commented Jul 11, 2010 at 6:22

6 Answers 6

9

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
Sign up to request clarification or add additional context in comments.

3 Comments

Does having the products inherit from the same parent class affect their variability? For example, could Bicycle's have a "frame_material" attribute and Popsicle's have a "flavor"?
@maerics There is no STI, all attributes come from each of the specific tables for the subclasses.
This is a really clean solution.
2

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.

5 Comments

Wait - why did you post two answers?
my commented answer was not in proper format thats why i give a seprate answer .
Thanks @Jamie Wong: for revisions actually i am the new guy of stackoverflow and i dont know about formatting text
If you need a lot of space to respond to a comment, edit your original answer and note that it's in response to a comment.
This strategy isn't quite what I'm looking for since when I have a Producer I need to know what kind of products it will have in order to decide whether to call p.bicycles or p.popsicles, I'd like to have a single method that returns the products (e.g. p.products).
1

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.

Comments

0

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.

Comments

0

Here is how I did it, which makes correct SQL queries:

class Message < ApplicationRecord
  module FromTypes
    USER = "User"
    PHONE_NUMBER = "PhoneNumber"
    ALL = [USER, PHONE_NUMBER]
  end

  module ToTypes
    USER = "User"
    PHONE_NUMBER = "PhoneNumber"
    ALL = [USER, PHONE_NUMBER]
  end

  belongs_to :from, polymorphic: true
  belongs_to :to, polymorphic: true

  scope :from_user, -> { where(from_type: FromTypes::USER) }
  scope :from_phone_number, -> { where(from_type: FromTypes::PHONE_NUMBER) }
  scope :to_user, -> { where(to_type: ToTypes::USER) }
  scope :to_phone_number, -> { where(from_type: ToTypes::PHONE_NUMBER) }

  validates :from_type, presence: true, inclusion: { in: FromTypes::ALL }
  validates :to_type, presence: true, inclusion: { in: ToTypes::ALL }

  def from_user?
    from_type == FromTypes::USER
  end

  def from_phone_number?
    from_type == FromTypes::PHONE_NUMBER
  end

  def to_user?
    to_type == ToTypes::USER
  end

  def to_phone_number?
    to_type == ToTypes::PHONE_NUMBER
  end
end
  has_many(
    :messages_sent,
    -> { from_phone_number },
    class_name: "Message",
    foreign_key: :from_id
  )
  has_many(
    :messages_received,
    -> { to_phone_number },
    class_name: "Message",
    foreign_key: :to_id
  )

Which makes SQL queries like this:

>> PhoneNumber.first.messages_received
  PhoneNumber Load (1.1ms)  SELECT "phone_numbers".* FROM "phone_numbers" ORDER BY "phone_numbers"."id" ASC LIMIT $1  [["LIMIT", 1]]
  Message Load (2.1ms)  SELECT "messages".* FROM "messages" WHERE "messages"."to_id" = $1 AND "messages"."from_type" = $2 /* loading for pp */ LIMIT $3  [["to_id", 1], ["from_type", "PhoneNumber"], ["LIMIT", 11]]

Comments

-3
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

2 Comments

Can you explain your answer a little, perhaps even modify it to use the producer/product terminology from the question?
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 using on object, then please leave comment on this with code .

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.