ruby-on-railspolymorphismruby-on-rails-6ruby-on-rails-7

Rails | Use Nested Attribute as Foreign Type For Polymorphic Association?


In Rails, I would like to structure my models such that the main record (lets call it Shipment) has a detail record (base type: ShipmentDetail) that is polymorphic such that the structure of the data can change based upon a nested attribute in another association (let's say: client.name).

The ShipmentDetail class contains a payload jsonb type field with client specific information. It can be sub-classed so that we can pull out that client-specific information easily, such as ClientA::ShipmentDetail, ClientB::ShipmentDetail, etc. In this way, all ShipmentDetail instances are stored in the same shipment_details table in the database.

Where I am having trouble is figuring out how to properly configure the association between Shipment and ShipmentDetail.

Here is the base ShipmentDetail class:

class ShipmentDetail < ApplicationRecord
  self.abstract_class = true
  self.table_name = "shipment_details"

  has_one :shipment
end

And here is how I have currently attempted to model the Shipment class:

class Shipment < ApplicationRecord
  belongs_to :client, optional: true

  belongs_to :details, class_name: :shipment_details, \
    polymorphic: true, foreign_key: :id, foreign_type: :client_name, optional: true

end

Unfortunately, if I attempt to assign something to the details association I get an error like:

ActiveModel::MissingAttributeError: can't write unknown attribute `client_name`

Please advise on how I can properly model this association - is there a trick to configuring the foreign_type to use a nested attribute? Thanks!


Solution

  • Your associations don't match with your description.

    the main record (lets call it Shipment) has a detail record (base type: ShipmentDetail) that is polymorphic such that the structure of the data can change based upon a nested attribute in another association (let's say: client.name).

    so Shipment has a ShipmentDetail, the associations should be:

    class Shipment < ApplicationRecord
      has_one :shipment_detail
    end
    
    class ShipmentDetail < ApplicationRecord
      belongs_to :shipment
      belongs_to :client
    end
    

    Client-speficic shipment details can be subclass, and client information can be pull-out:

    class ClientAShipmentDetail < ShipmentDetail
      def client_data
        if payload['client'] == 'clientA'
         # return Client A data
        end
      end
    end
    
    class ClientBShipmentDetail < ShipmentDetail
      def client_data
        if payload['client'] == 'clientB'
         # return Client B data
        end
      end
    end
    

    In this design, you can get a dynamic instance of Shipment Detail specific class by using (Single Table Inheritance)

    Table shipment_details just need to add a string column name type so that when shipment_detail association is loaded, it will return correct object of class type.

    Example data:

    shipments table
    id|name|
    1|shipment1
    2|shipment2
    
    shipment_details table
    id|shipment_id|type|
    1|1|ClientAShipmentDetail
    2|2|ClientBShipmentDetail
    
    
    shipmment1 = Shipment.find(1)
    shipment1.shipment_detail # Return instance of ClientAShipmentDetail class
    
    shipmment2 = Shipment.find(2)
    shipment2.shipment_detail # Return instance of ClientBShipmentDetail class