简体   繁体   中英

Rails 4: Has_one polymorphic association not working

I have two models Purchase and Address . I'm trying to make Address polymorphic so I can reuse it in my Purchase model for has_one :billing_address and has_one :shipping_address . Here's my schema:

create_table "addresses", force: true do |t|
  t.string   "first_name"
  t.string   "last_name"
  t.string   "street_address"
  t.string   "street_address2"
  t.string   "zip_code"
  t.string   "phone_number"
  t.datetime "created_at"
  t.datetime "updated_at"
  t.integer  "state_id"
  t.string   "city"
  t.string   "addressable_type" #<-- 
  t.integer  "addressable_id"   #<--
end

address model:

class Address < ActiveRecord::Base
  belongs_to :addressable, polymorphic: true
  ...
end

purchase model:

class Purchase < ActiveRecord::Base
  has_one :shipping_address, as: :addressable
  has_one :billing_address, as: :addressable
  ...
end

Everything looks fine to me, but my Rspec tests fail:

    Failures:

      1) Purchase should have one shipping_address
         Failure/Error: it { should have_one(:shipping_address) }
           Expected Purchase to have a has_one association called shipping_address (ShippingAddress does not exist)
         # ./spec/models/purchase_spec.rb:22:in `block (2 levels) in <top (required)>'

      2) Purchase should have one billing_address
         Failure/Error: it { should have_one(:billing_address) }
           Expected Purchase to have a has_one association called billing_address (BillingAddress does not exist)
         # ./spec/models/purchase_spec.rb:23:in `block (2 levels) in <top (required)>'

It doesn't seem to be detecting that the association is polymorphic. Even in my console:

    irb(main):001:0> p = Purchase.new
    => #<Purchase id: nil, user_id: nil, order_date: nil, total_cents: 0, total_currency: "USD", shipping_cents: 0, shipping_currency: "USD", tax_cents: 0, tax_currency: "USD", subtotal_cents: 0, subtotal_currency: "USD", created_at: nil, updated_at: nil, status: nil>
    irb(main):002:0> p.shipping_address
    => nil
    irb(main):003:0> p.build_shipping_address
    NameError: uninitialized constant Purchase::ShippingAddress

What am I doing wrong??

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 ie, :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

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.

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, ie, 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM