Rails Polymorphic has_many
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. 开发者_C百科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.
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
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.
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.
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.
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
精彩评论