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
Rails Cookbook (Cookbooks (OReilly))
ISBN: 0596527314
EAN: 2147483647
Year: 2007
Pages: 250
Authors: Rob Orsini

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net