I have two models Purchase
and Address
. I\'m trying to make Address
polymorphic so I can reuse it in my Purchase
model fo
You need to specify the :class_name
option for the has_one
association, as the class name can't be inferred from the association name i.e., :shipping_address
and :billing_address
in your case doesn't give an idea that they refer to class Address
.
Update the Purchase
model as below:
class Purchase < ActiveRecord::Base
has_one :shipping_address, class_name: "Address", as: :addressable
has_one :billing_address, class_name: "Address", as: :addressable
## ...
end
It is somewhat of a convention to name the polymorphic relation something that ends in "able". This makes sense if it conveys some sort of action. For example and image is "viewable", an account is "payable", etc. However, sometimes it is hard to come up with a term that ends in "able" and when forced to find such a term, it can even cause a bit of confusion.
Polymorphic associations often show ownership. For this case in particular, I would would define the Address model as follows:
class Address < ActiveRecord::Base
belongs_to :address_owner, :polymorphic => true
...
end
Using :address_owner
for the relation name should help clear up any confusion as it makes it clear that an address belongs to something or someone. It could belong to a user, a person, a customer, an order, or a business, etc.
I would argue against single table inheritance as shipping and billing addresses are still at the end of the day, just addresses, i.e, there is no morphing going on. In contrast, a person, an order, or a business are cleary very different in nature and therefore make a good case for polymorphism.
In the case of an order, we still want the columns address_owner_type
and address_owner_id
to reflect that an order belongs to a customer. So the question remains: How do we show that an order has a billing address and a shipping address?
The solution that I would go with is to add foreign keys to the orders table for the shipping address and billing address. The Order class would look something like the following:
class Order < ActiveRecord::Base
belongs_to :customer
has_many :addresses, :as => :address_owner
belongs_to :shipping_address, :class_name => 'Address'
belongs_to :billing_address, :class_name => 'Address'
...
end
Note: I am using Order for the class name in favor of Purchase, as an order reflects both the business and customer sides of a transaction, whereas, a purchase is something that a customer does.
If you want, you can then define the opposite end of the relation for Address:
class Address < ActiveRecord::Base
belongs_to :address_owner, :polymorphic => true
has_one :shipping_address, :class_name => 'Order', :foreign_key => 'shipping_address_id'
has_one :billing_address, :class_name => 'Order', :foreign_key => 'billing_address_id'
...
end
The one other bit of code to clear up yet, is how to set the shipping and billing addresses? For this, I would override the respective setters on the Order model. The Order model would then look like the following:
class Order < ActiveRecord::Base
belongs_to :customer
has_many :addresses, :as => :address_owner
belongs_to :shipping_address, :class_name => 'Address'
belongs_to :billing_address, :class_name => 'Address'
...
def shipping_address=(shipping_address)
if self.shipping_address
self.shipping_address.update_attributes(shipping_address)
else
new_address = Address.create(shipping_address.merge(:address_owner => self))
self.shipping_address_id = new_address.id # Note of Caution: Replacing this line with "self.shipping_address = new_address" would re-trigger this setter with the newly created Address, which is something we definitely don't want to do
end
end
def billing_address=(billing_address)
if self.billing_address
self.billing_address.update_attributes(billing_address)
else
new_address = Address.create(billing_address.merge(:address_owner => self))
self.billing_address_id = new_address.id
end
end
end
This solution solves the problem by defining two relations for an address. The has_one
and belongs_to
relation allows us to track shipping and billing addresses, whereas the polymorphic
relation shows that the shipping and billing addresses belong to an order. This solution gives us the best of both worlds.
I think you've misunderstood what polymorphic associations are for. They allow it to belong to more than one model, Address here is always going to belong to Purchase.
What you've done allows an Address to belong to say, Basket or Purchase. The addressable_type is always going to be Purchase. It won't be ShippingAddress or BillingAddress which I think you think it will be.
p.build_shipping_address
doesn't work because there isn't a shipping address model.
Add class_name: 'Address'
and it will let you do it. However it still won't work the way you expect.
I think what you actually want is single table inheritance. Just having a type column on address
class Purchase < ActiveRecord::Base
has_one :shipping_address
has_one :billing_address
end
class Address < ActiveRecord::Base
belongs_to :purchase
...
end
class ShippingAddress < Address
end
class BillingAddress < Address
end
This should be fine because shipping and billing address will have the same data, if you've got lots of columns that are only in one or the other it's not the way to go.
Another implementation would be to have shipping_address_id and billing_address_id on the Purchase model.
class Purchase < ActiveRecord::Base
belongs_to :shipping_address, class_name: 'Address'
belongs_to :billing_address, class_name: 'Address'
end
The belongs_to :shipping_address
will tell rails to look for shipping_address_id with the class name telling it to look in the addresses table.