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.
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
Related
Iam setting up user accounts and users have different roles. Iam using STI and my models look like:
class User < ActiveRecord::Base
end
Class Teacher < User
end
Class Student < User
end
How can i set up a many to many relationship between Students and Teachers so that i can do calls like,
Teacher.students or Student.teachers ??
Teac
I'm putting aside the 'whether or not to use STI?' question and assuming you have a reason.
You'll need an id column of course, eg teacher_id.
I'm going to include explicit class names in my example because I know it works, but I probably only need them because my models are namespaced, so they are probably superfluous for you.
class Teacher < User
has_many :students, :class_name => 'Student', :foreign_key => 'teacher_id'
end
class Student < User
belongs_to :teacher, :class_name => 'Teacher', :foreign_key => 'teacher_id'
end
And that should do it---dang. That would do it except I missed many-to-many.
So I'd try HABTM, but I confess I've never actually done this with STI.
Start with this migration to make join table.
class CreateStudentsAndTeachers < ActiveRecord::Migration
def change
create_table :students_teachers do |t|
t.belongs_to :student
t.belongs_to :teacher
end
end
end
And then this should work (and again, you may well not need the specified class name)
class Teacher < User
has_and_belongs_to_many :students, :class_name => 'Student'
end
class Student < User
has_and_belongs_to_many :teachers, :class_name => 'Teacher'
end
I am currently trying to create a custom method on a model, where the conditions used are those of a has_many association. The method I have so far is:
class Dealer < ActiveRecord::Base
has_many :purchases
def inventory
inventory = Vehicle.where(:purchases => self.purchases)
return inventory
end
end
This is not working, due to the fact that Vehicle has_many :purchases (thus there is no column "purchases" on the vehicle model). How can I use the vehicle.purchases array as a condition in this kind of query?
To complicate matters, the has_many is also polymorphic, so I can not simply use a .join(:purchases) element on the query, as there is no VehiclePurchase model.
EDIT: For clarity, the relevant parts of my purchase model and vehicle models are below:
class Purchase < ActiveRecord::Base
attr_accessible :dealer_id, :purchase_type_id
belongs_to :purchase_item_type, :polymorphic => true
end
class Vehicle < ActiveRecord::Base
has_many :purchases, :as => :purchase_item_type
end
class Dealer < ActiveRecord::Base
def inventory
Vehicle.where(:id => purchases.where(:purchase_item_type_type => "Vehicle").map(&:purchase_item_type_id))
end
end
Or:
def inventory
purchases.includes(:purchase_item_type).where(:purchase_item_type_type => "Vehicle").map(&:purchase_item_type)
end
I was able to do this using the :source and :source_type options on the Vehicle model, which allows polymorphic parents to be associated.
I have the models shown below. I need to store some details that are specific a person and a house (first_viewed:date, opening offer:decimal, etc). I feel like these should belong to the PersonHouse model but I'm not certain enough. Any suggestions?
class Person < ActiveRecord::Base
has_many :houses, through: :person_houses
has_one :favorite_house, through: :person_houses
end
class PersonHouse < ActiveRecord::Base
belongs_to :house
belongs_to :person
end
class House < ActiveRecord::Base
has_many :house_people
has_many :people, through: :person_houses
end
I could do something like this to get all the details but perhaps there is a more effient way.
#house = House.find(1)
#house.house_people.each do |hp|
puts hp.person.name
puts hp.first_viewed
puts #house.address
end
I think your assumption is correct. If the data is relevant to the relationship between a person and a house, then yes it belongs on this model. The only recommendation I would make is to rename this model to a name that better describes what the relationship is. It doesn't have to be the concatenation of the two models it joins. I don't know exactly what the model is going to be ultimately used for, but SelectedHouse, HouseProspect or something along those lines might work.
You can also delegate properties to the house or person models:
class PersonHouse < AR::Base
belongs_to :person
belongs_to :house
delegate :address, :to => :house, :prefix => true
delegate :name, :to => :person, :prefix => true
end
person_house.address
person_house.person_name
A Person can have many Events and each Event can have one polymorphic Eventable record. How do I specify the relationship between the Person and the Eventable record?
Here are the models I have:
class Event < ActiveRecord::Base
belongs_to :person
belongs_to :eventable, :polymorphic => true
end
class Meal < ActiveRecord::Base
has_one :event, :as => eventable
end
class Workout < ActiveRecord::Base
has_one :event, :as => eventable
end
The main question concerns the Person class:
class Person < ActiveRecord::Base
has_many :events
has_many :eventables, :through => :events # is this correct???
end
Do I say has_many :eventables, :through => :events like I did above?
Or do I have to spell them all out like so:
has_many :meals, :through => :events
has_many :workouts, :through => :events
If you see an easier way to accomplish what I'm after, I'm all ears! :-)
You have to do:
class Person < ActiveRecord::Base
has_many :events
has_many :meals, :through => :events, :source => :eventable,
:source_type => "Meal"
has_many :workouts, :through => :events, :source => :eventable,
:source_type => "Workout"
end
This will enable you to do this:
p = Person.find(1)
# get a person's meals
p.meals.each do |m|
puts m
end
# get a person's workouts
p.workouts.each do |w|
puts w
end
# get all types of events for the person
p.events.each do |e|
puts e.eventable
end
Another option of this is to use a Single Table Inheritance (STI) or Multi Table Inheritance (MTI) pattern, but that requires some ActiveRecord/DB Table rework, but this may help others still finding this who are designing it for the first time.
Here is the STI method in Rails 3+:
Your Eventable concept becomes a class and needs a type column (which rails automatically populates for you).
class Eventable < ActiveRecord::Base
has_one :event
end
Then, your other two classes inherit from Eventable instead of AR::Base
class Meal < Eventable
end
class Workout < Eventable
end
And your event object is basically the same, just not polymorphic:
class Event < ActiveRecord::Base
belongs_to :person
belongs_to :eventable
end
This may make some of your other layers more confusing, if you've never seen this before and you're not careful. For example, a single Meal object can be accessed at /meals/1 and /eventable/1 if you make both endpoints available in the routes, and you need to be aware of the class you're using when you pull an inherited object (hint: the becomes method may be very useful if you need to override the default rails behavior)
But this is a much cleaner deliniation of responsibilities as apps scale, in my experience. Just a pattern to consider.
I have a model called Purchase and a model called TicketType. A purchase can have many ticket types, and a ticket type can have many purchases.
So I have
class Purchase < ActiveRecord::Base
has_many :purchases_ticket_types, :class_name => 'PurchaseTicketType'
has_many :ticket_types, :through => :purchases_ticket_types
end
class TicketType < ActiveRecord::Base
has_many :purchases_ticket_types, :class_name => 'PurchaseTicketType'
has_many :purchases, :through => :purchases_ticket_types
end
class PurchaseTicketType < ActiveRecord::Base
set_table_name "purchases_ticket_types"
belongs_to :purchase
belongs_to :ticket_type
end
I knew purchases_ticket_types for a table name was going to be trouble as soon as ActiveRecord failed to identify the correct table name from the camel-cased class name. I ended having to call set_table_name.
But the worst part is having to do this:
purchase = Purchase.find(1)
purchase.purchases_ticket_types.each do |purchase_ticket_type|
puts 'join contains quantity: ' + purchase_ticket_type.quantity
puts 'ticket type name is: ' + purchase_ticket_type.ticket_type.name
end
Look how verbose and repetitive that reads. Everything is working but it looks so ugly. Is there a better way of naming many-to-many associations to avoid this kind of thing: purchase.purchases_ticket_types.first.ticket_type.name ?
Thanks!
How about using TicketSale as the join model name (or simply Sale if your application doesn't sell anything else apart from tickets):
class Purchase < ActiveRecord::Base
has_many :ticket_sales
has_many :ticket_types, :through => :ticket_sales
end
class TicketType < ActiveRecord::Base
has_many :ticket_sales
has_many :purchases, :through => :ticket_sales
end
class TicketSale < ActiveRecord::Base
belongs_to :purchase
belongs_to :ticket_type
end