25
Jun
08

named_scope with acts_as_tree

I fairly often use the acts_as_tree plugin in my applications.  While acts_as_nested_set (and superior variants..) is more powerful, often times a simple two-level deep hierarchy is all I need and acts_as_tree is simple.  I’ve found the new named_scope functionality in Rails 2.1 to be very helpful when dealing with tree data structures.

Firstly, it’s somewhat rare that I have one single root node in the tree structure (which is apparently how it’s meant to be used).  Instead I’ll have multiple “parent” nodes designated with a NULL parent_id and children beneath.  In the past I’ve always done something like: find(:all, :conditions => {:parent_id => nil}) to grab the top entries.  Instead with nested set you can do this:

named_scope :top, :conditions => {:parent_id => nil}

#Then:
Category.top

Another common task when dealing with hierarchical categories is to query on the base object (products for example) for members that are “part of” that category. Specifically, part of in a 2-level heirarchy simply means “where id = ? or parent_id = ?” on the joined categories table. Because this involves a join it was somewhat clunky to do before. Now with nested set, on the product model I can declare this:

named_scope :in_category, lambda {|c| {:include => [:category], :conditions => ["categories.id = ? OR categories.parent_id = ?", c, c]} }

# Then:
Product.in_category(3)

I would also highly encourage you all to check out RailsCasts Episode 112 “Anonymous Scopes” by Ryan Bates where he outlines a pattern for handling conditions elegantly with searches using named_scopes in Rails 2.1, something I’ve always found to be lacking from Rails core and always resorted to using plugins like criteria_query.


12 Responses to “named_scope with acts_as_tree”


  1. 1 mongo Jul 16th, 2008 at 6:34 am

    thank you so much; i was looking for an example with the lambda *and* the association.

  2. 2 essetLaf Aug 3rd, 2008 at 12:04 pm

    Thank you

  3. 3 Martin Aug 29th, 2008 at 10:15 am

    For the first named_scope you could have also used the class method “root” baked into acts_as_tree:

    Category.root

  4. 4 benhughes Aug 29th, 2008 at 12:19 pm

    You could but that’s an actual find method that returns results, not a scope. My top scope above can be chained. Also, Category.root just returns the *first* result with parent_id = nil (for the case where you have a category with a single root). There is however a Category.roots that works the same way as my top, just not with a named_scope.

  5. 5 Javix Feb 10th, 2009 at 9:55 am

    Hi, Ben! I’d like to use acts_as_tree plusgin but I can’t find any examples how to create a new sub-category inside of the given category(i.e. how to pass parent_id to ‘enw-create-edit-update actions).Could you post a simple example, please.Thank you.

  6. 6 Camilo Sáchez Jul 29th, 2009 at 3:24 am

    Hi Ben and thank you so much for this post!

    But I have a questions.

    How can this?

    Video Games (15)
    Nintendo DS (3)
    DS Games (1)
    DS Lite Consoles (2)
    Nintendo Wii (2)
    Wii Consoles (1)
    Wii Games (1)
    PlayStation (10)
    PS2 Consoles (6)
    PS3 Accessories (4)

    With you example i have two levels

    I need four or five levels – recursive categories

    (I need this bad sample)
    :conditions => [“categories.id = ? OR categories.parent_id OR categories.grandparent_id = ? OR .. (etc..)

    How can count total Products in each parent level?

    CategoriesHelper code:

    def find_all_listsubcategories(category)
    if category.children.size > 0
    ret = ”
    category.children.each { |subcat|
    ret += ”
    ret += link_to h(subcat.name), :action => ‘show’, :id => subcat
    ret += ”
    }
    ret += ”
    end
    end

    Can you help me please, I’m not a developer. I’m Web designer

    Sorry, i don’t speak english very well

    thank you again!

  7. 7 Camilo Sáchez Jul 29th, 2009 at 3:27 am

    Hi, in my las comment the categories tree are indented for subcategories…

    Video Games (15)
    ——Nintendo DS (3)
    ———–DS Games (1)
    ———–DS Lite Consoles (2)
    ——Nintendo Wii (2)
    ———–Wii Consoles (1)
    ———-Wii Games (1)
    ——PlayStation (10)
    ———-PS2 Consoles (6)
    ———-PS3 Accessories (4)

  8. 8 benhughes Jul 29th, 2009 at 11:11 am

    Doing heirarchies more than one sub-level deep cannot be worked with easily since it requires recursive operations to do things like listing *all* children of a given top-level category. In your example above that’s fine and in fact for what you’re doing, since you have to iterate over everything anyways, I’d simply calculate the counts of only the leaf nodes and then calculate in Ruby the counts of parent nodes up from there.

    Anyways I can recommend a different approach to heirarchies called the nested set model (http://dev.mysql.com/tech-resources/articles/hierarchical-data.html). With rails there are several plugins implementing this, just search on GitHub for “nested_set”.

  9. 9 elDub Mar 8th, 2010 at 5:01 pm

    Hello Ben,

    Have you had any difficulties with destroying jobs in an acts_as_tree relationship when going through named scopes? Using your example I have been finding that:

    Category.top.destroy_all

    will not destroy the child categories because it seems to apply the “top” conditions to the children collection.

    However, if I were to do

    Category.find(:conditions => { :parent_id => nil }).destroy_all

    all is good.

  10. 10 elDub Mar 8th, 2010 at 5:06 pm

    … I really should read my code before I post.

    The “working” statement would be

    Category.find(:all, :conditions => { :parent_id => nil }).each{ |category| category.destroy }

  11. 11 Jean Combs Dec 24th, 2010 at 9:36 am

    Hello Ben, Have you had any difficulties with destroying jobs in an acts_as_tree relationship when going through named scopes? Using your example I have been finding that: Category.top.destroy_all will not destroy the child categories because it seems to apply the “top” conditions to the children collection. However, if I were to do Category.find(:conditions => { :parent_id => nil }).destroy_all all is good.

  12. 12 benhughes Dec 24th, 2010 at 4:03 pm

    Doing Category.top.destroy_all should absolutely work, after all “top” is merely a named scope for “:conditions => {:parent_id => nil}”. Since I’ve posted this there have been many full-featured acts_as_tree replacements that already have such scopes and work with Rails 3.

Leave a Reply