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.
: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:
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.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.