Reputation: 156434
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.
Upvotes: 15
Views: 18303
Reputation: 9085
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]]
Upvotes: 0
Reputation: 501
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.
Upvotes: 2
Reputation: 16435
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
Upvotes: 9
Reputation: 10089
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.
Upvotes: 0
Reputation: 156434
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.
Upvotes: 1
Reputation: 501
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
Upvotes: -3