Search code examples
ruby-on-railstreehierarchynested-sets

Rails: Multiple trees for a single item


I want to categorize objects in multiple trees to reflect their characteristics and to build a navigation on.

So, given the following trees:

Category1
-Category-1-1
-Category-1-2

Category2
-Category-2-1
-Category-2-2
--Category-2-2-1

An object could e.g. belong to both Category-1-2 and to Category-2-2-1.

The goal is to be able to fetch all objects from the database

  • that belong to a certain category
  • that belong to a certain category or its decendants

A more practical example:

A category might have a hierarchy of 'Tools > Gardening Tools > Cutters'.

A second category: 'Hard objects > Metal objects > Small metal objects'

An object 'Pruners' would be categorized as belonging to 'Cutters' as well as 'Small metal objects'.

I want to be able to

  • retrieve all 'Gardening Tools' -> 'Pruners'
  • retrieve all Category children of 'Gardening Tools' -> 'Cutters'
  • retrieve all 'Hard objects' -> 'Pruners'
  • retrieve all 'Hard objects' that are also 'Cutters' -> 'Pruners'
  • retrieve all 'Soft objects' that are also 'Cutters' -> [] Any pointers? I have briefly looked at closure_tree, awesome_nested_sets etc., but I am not sure they are a good match.

Solution

  • I just did this and I chose not to use ancestry, but closure_tree because the author says it is faster and I agree with him. Know you need a `has_and_belongs_to_many' between Categories (which I like to call tags whenever I add multiple to a single object) and Objects.

    Now the finders, the bad news is that without your own custom query you might not be able to do it with one. Using the gems methods you will do something like:

    Item.joins(:tags).where(tags: {id: self_and_descendant_ids })
    

    The code is clean and it executes two queries, one for the descendant_ids and another one in Objects. Slight variations of this, should give you what you need for all except the last. That one is tough and I haven't implemented it (I'm in the process).

    For now, you will have to call tag.self_and_ancestor_ids on both (Query count: 2), all items in those tags (Query count: 4) and intersect. After this, some serious refactoring is needed. I think we need to write SQL to reduce the number of queries, I don't think Rails query interface will be enough.

    Another reason I chose *closure_tree* was the use of parent_id, all siblings share it (just like any other Rails association) so it made it easier to interface with other gems (for example RankedModel to sort).