Rails 4: Has_one polymorphic association not working

后端 未结 3 804
耶瑟儿~
耶瑟儿~ 2020-12-20 06:52

I have two models Purchase and Address. I\'m trying to make Address polymorphic so I can reuse it in my Purchase model fo

相关标签:
3条回答
  • 2020-12-20 07:36

    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
    
    0 讨论(0)
  • 2020-12-20 07:44

    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.

    0 讨论(0)
  • 2020-12-20 07:45

    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.

    0 讨论(0)
提交回复
热议问题