Self referencing objects

My application has products, and each product can have components which are themselves products. I achieve this via a pair of self-referencing has_and_belongs_to_many relationships.

  has_and_belongs_to_many :components,
    :class_name => 'Product',
    :join_table => "product_grouping",
    :association_foreign_key => "component_product_id",
    :foreign_key => "group_product_id"

  has_and_belongs_to_many :groups,
    :class_name => 'Product',
    :join_table => "product_grouping",
    :association_foreign_key => "group_product_id",
    :foreign_key => "component_product_id"

Note the presence of the join table product_grouping which has two fields: group_product_id and component_product_id

I wanted to be able to collect together all components within a product hierarchy. So if my products are car, engine and piston: piston is a component of engine, which is a component of car. However, car.components would only return engine. I needed a method that would traverse the whole component hierarchy.

I achieved this by using a method that referenced itself within a collect:

  def all_components
    components.collect{|c| [c] << c.all_components}.flatten.uniq
  end

I’ve seen this technique used before, but always had a little difficulty getting my head around it. I found that the key to getting it working was to make sure that the method always returned an array or nil.

Now:

  car.components -> [engine]
  car.all_components -> [engine, piston]

What effectively is happening with car.all_components is this:

  • we first call all_components on car.
  • it works through each component and returns that component plus what ever is returned by its all_components. So as there is only one component (engine) engine.all_components is called, its results are returned in an array with a copy of itself.
  • engine.all_components acts on its only component piston. So it returns piston and the results of piston.all_components
  • piston has no components, so piston.all_components returns nil and the process ends.
  • flatten then gets rid of any array within array issues.
  • uniq tidies up instances where two products may share the same component.
This entry was posted in Ruby. Bookmark the permalink.