Recipe 3.17. Creating a Directory of Nested Topics with acts_as_tree
Rails Cookbook
Authors: Orsini R
Published year: 2007
Pages: 50/250
Buy this book on amazon.com >>

Recipe 3.17. Creating a Directory of Nested Topics with acts_as_tree

Problem

Database tables are simply a set of rows. However, you often want those rows to behave in some other way. If the data in your table represents a tree structure, how do you work with it as a tree?

For example, you have a web site organized by topic. Topics can have subtopics, as can the subtopics themselves . You want to model these topics as a tree structure and store them in a single database table.

Solution

First, create a topics table that includes a parent_id column, and populate it with some topics. Use the following migration for this:

db/migrate/001_create_topics.rb :

class CreateTopics < ActiveRecord::Migration
  def self.up
    create_table :topics do t
      t.column :parent_id, :integer
      t.column :name,      :string
    end

    Topic.create :name => 'Programming and Development'
    Topic.create :parent_id => 1, :name => 'Algorithms'
    Topic.create :parent_id => 1, :name => 'Methodologies'
    Topic.create :parent_id => 3, :name => 'Extreme Programming'
    Topic.create :parent_id => 3, :name => 'Object-Oriented Programming'
    Topic.create :parent_id => 3, :name => 'Functional Languages'
    Topic.create :parent_id => 2, :name => 'Sorting'
    Topic.create :parent_id => 7, :name => 'Bubble sort'
    Topic.create :parent_id => 7, :name => 'Heap sort'
    Topic.create :parent_id => 7, :name => 'Merge sort'
    Topic.create :parent_id => 7, :name => 'Quick sort'
    Topic.create :parent_id => 7, :name => 'Shell sort'
  end

  def self.down
    drop_table :topics
  end
end

Declare that this model is to act as a tree structure:

app/models/topic.rb :

class Topic < ActiveRecord::Base
  acts_as_tree :order => "name" 
end

Discussion

Calling the acts_as_tree class method on a model gives instances of that model some additional methods for inspecting the their relationships within the tree. These methods include:



siblings

Returns an array that contains the other children of a node's parent



self_and_siblings

Same as siblings but includes the node of the caller as well



ancestors

Returns an array of all the ancestors of the calling node



root

Returns the root node (the node with no further parent nodes) of the caller's tree

Let's open up a Rails console session and inspect the topics tree that was created by the solution.

First, get the root node, which we know has a parent_id of null :

>>

root = Topic.find(:first, :conditions => "parent_id is null")

=> #<Topic:0x4092ae74 @attributes={"name"=>"Programming and Development",
"id"=>"1", "parent_id"=>nil}>

We can show the root node's children with:

>>

root.children

=> [#<Topic:0x4090da04 @attributes={"name"=>"Algorithms", "id"=>"2",
"parent_id"=>"1"}>, #<Topic:0x4090d9c8
@attributes={"name"=>"Methodologies", "id"=>"3", "parent_id"=>"1"}>]

The following returns a hash of the attributes of the first node in the root node's array of children:

>>

root.children.first.attributes

=> {"name"=>"Algorithms", "id"=>2, "parent_id"=>1}

We can find a leaf node from the root by alternating calls to children and first . From the leaf node, a single call to root finds the root node:

>>

leaf = root.children.first.children.first.children.first

=> #<Topic:0x408dd804 @attributes={"name"=>"Bubble sort", "id"=>"8",
"parent_id"=>"7"}, @children=[]>

>>

leaf.root

=> #<Topic:0x408cffd8 @attributes={"name"=>"Programming and Development",
"id"=>"1", "parent_id"=>nil}, @parent=nil>

In addition to the topics loaded in the solution, we can create more directly from the Rails console. Let's create another root node named Shapes and give it two children nodes of its own:

>>

r = Topic.create(:name => "Shapes")

=> #<Topic:0x4092e9d4 @attributes={"name"=>"Shapes", "id"=>13,
"parent_id"=>nil}, @new_record_before_save=false,
@errors=#<ActiveRecord::Errors:0x4092baf4 @errors=,
@base=#<Topic:0x4092e9d4 ...>>, @new_record=false>

>>

r.siblings

=> [#<Topic:0x4092508c @attributes={"name"=>"Programming and Development",
"id"=>"1", "parent_id"=>nil}>]

>>

r.children.create(:name => "circle")

=> #<Topic:0x40921ab8 @attributes={"name"=>"circle", "id"=>14,
"parent_id"=>13}, @new_record_before_save=false,
@errors=#<ActiveRecord::Errors:0x40921108 @errors=,
@base=#<Topic:0x40921ab8 ...>>, @new_record=false>

>>

r.children.create(:name => "square")

=> #<Topic:0x4091c57c @attributes={"name"=>"square", "id"=>15,
"parent_id"=>13}, @new_record_before_save=false,
@errors=#<ActiveRecord::Errors:0x4091bbcc @errors=,
@base=#<Topic:0x4091c57c ...>>, @new_record=false>

From mysql , we can verify that the three new elements were added to the database as expected.

mysql>

select * from topics;

+----+-----------+-----------------------------+
 id  parent_id  name                        
+----+-----------+-----------------------------+
  1       NULL  Programming and Development 
  2          1  Algorithms                  
  3          1  Methodologies               
  4          3  Extreme Programming         
  5          3  Object-Oriented Programming 
  6          3  Functional Languages        
  7          2  Sorting                     
  8          7  Bubble sort                 
  9          7  Heap sort                   
 10          7  Merge sort                  
 11          7  Quick sort                  
 12          7  Shell sort                  
 13       NULL  Shapes                      
 14         13  circle                      
 15         13  square                      
+----+-----------+-----------------------------+
15 rows in set (0.00 sec)

acts_as_tree and acts_as_nested_set are significantly different from each other, even though they appear to do the same thing. acts_as_tree scales much better with a big table because each row does not have to be updated when a new row is added. With acts_as_nested_set , position information for each record has to be updated whenever an item is added or removed.

The default behavior is to use a column named parent_id to store parent nodes in the tree. You can change this behavior by specifying a different column name with the :foreign_key option of the acts_as_tree options hash.

See Also

  • A Treemap library for Ruby, http:// rubyforge .org/projects/rubytreemap; for a tutorial on using the Treemap library with Rails, see http://blog.tupleshop.com/2006/7/27/treemap-on-rails


Rails Cookbook
Authors: Orsini R
Published year: 2007
Pages: 50/250
Buy this book on amazon.com >>

Similar books on Amazon