Words and Code

One writer’s journey from words to code.

It’s All in the Family: Using Acts_as_tree

#technicaltuesdays, rails


When working in Rails, it’s all the family – literally. No matter the size of your application, almost all of your objects are going to be related to each other. You can create however many objects you want because database rows are cheap, cheap, cheap! But the more objects you make, the harder it is to keep track of the other data that the object relates to (which is generally yet another object).

I found myself in such a predicament last week, when I had to make numerous objects relate to one other to create a tree structure. The obvious first approach was to use the belongs_to and has_many relationship. But when I realized that I wanted some Genre objects to belong to other Genre objects, I ran into a problem. Depending solely on the ActiveRecord relationships turned out to be painful, messy, and complicated, and wouldn’t make my code very flexible or sustainable over time.

So I Googled around and found a handy plugin created by DHH himself called acts_as_tree. This gem allows you to create a hierarchical structure of objects in your application and – to take it a step further – gives you a bunch of incredibly helpful methods. It even allows you to visualize your tree structure! Sound amazing? That’s because it is. And if you follow a few easy steps, you can use it in your application, too.

Family Ties

So, for this post I’ll continue working through my basic eCommerce Bookstore application, which I’ve been using as an example for my previous posts. I’ve already got some Book objects, but as my store starts to grow, it’s going to be pretty hard to keep track of the different genres of Book objects that I currently have available.

The first step to tackling this problem was easy: create Genre objects, each of which has_many different Book objects associated with it, while each Book object will belong_to one specific Genre.

But what about genres that are associated with and “descend from” other genres? Well, here’s where the delightful and easy-to-use acts_as_tree gem comes in.


First things first: we’ll add gem 'acts_as_tree' to our Gemfile.

Next, we need to add a column to our Genre database. We can write a simple migration that will add a parent_id integer to our database, which will allow us to find the parents and children of a Genre object:

1
2
3
4
5
class AddParentIdColumnToGenre < ActiveRecord::Migration
  def change
    add_column :genres, :parent_id, :integer
  end
end

Finally, we’ll head over to our Genre model, which is what we needs to act as a (family) tree. We need to add a single line in here, which implements the ActiveRecord plugin and specifies what we’ll be ordering our Genre objects by:

1
2
3
4
5
class Genre < ActiveRecord::Base
  has_many :books
  validates_presence_of :name
  acts_as_tree order: "name"
end

Blood Is Thicker Than Water

Okay, now let’s see this baby in action! We can start by making a root Genre object, and then giving it some children:

1
2
3
4
literature = Genre.create("name" => "Literature")

non_fiction = literature.children.create("name" => "Non-Fiction")
fiction = literature.children.create("name" => "Fiction")

Cool, but our tree doesn’t really look like a tree yet. Let’s give our non_fiction and fiction genres some children, grandchildren, and great-grandchildren of their own:

1
2
3
4
5
6
7
biography = non_fiction.children.create("name" => "Biography")
comic_novel = fiction.children.create("name" => "Comic Novel")
black_comedy = comic_novel.create("name" => "Black Comedy")
parody = comic_novel.create("name" => "Parody")
romantic_comedy = comic_novel.create("name" => "Romanic Comedy")
satire = comic_novel.create("name" => "Satire")
poltical_satire = satire.create("name" => "Political Satire")

Damn. Okay, well now our tree should look look less like a sprout and more like this bad boy:

It Runs In The Family

Even though we’ve created all these parent-child relationships, what can we do with them, exactly? Well, a lot! You can call the parent and children methods to get a full list of all the objects associated with a particular Genre instance:

1
2
3
4
5
6
7
8
9
10
11
12
13
literature.parent                   # => nil
fiction.parent                      # => literature
literature.children                 # => [non_fiction, fiction]
literature.children.first.children  # => [Biography]

literature.root?                    # => true
fiction.root?                       # => false
biography.leaf?                     # => true

black_comedy.siblings               # => [parody, romantic_comedy, satire]
poltical_satire.ancestors           # => [satire, comic_novel, fiction, literature]

Genre.root                          # => literature

An important thing to note here is that the children method will return an array of objects, even if there’s only one child! So if you’re trying to get one particular object, remember to call the first method in the array, or search by a Genre object’s specific id to avoid annoying bugs.

Some other cool methods to try include:

  1. leaves, a class method that will return all the “leaves” of the tree (in an array).

  2. descendants, an instance method that will return all the children, and the children’s children of an object (in an array).

  3. self_and_siblings, which returns the receiver object, as well as any siblings it may have (in an array).

  4. default_tree_order, which returns all the objects listed in alphabetical order!

But the coolest feature of the acts_as_tree gem is Tree View, which allows you to see a visualization of your entire tree. All we have to do view this magic is add this line to our Genre model:

1
extend ActsAsTree::TreeView

And then, call the class method tree_view, which takes in an attribute parameter:

1
Genre.tree_view(:name)

The resulting return value is pure flora magic:

1
2
3
4
5
6
7
8
9
10
Literature
 |_ Non-Fiction
 |    |_ Biography
 |_ Fiction
 |    |_ Comic Novel
 |        |_ Black Comedy
 |        |_ Parody
 |        |_ Romantic Comedy
 |        |_ Satire
 |            |_ Political Satire

Isn’t it so beautiful?! Doesn’t it make you feel like this:


Or maybe it’s just me.

tl;dr?

  • A lot of people seem to like the ancestry gem, but I think that acts_as_tree is a good one to start off with. If you need the extra functionality that ancestry provides, then you can eventually level up to that. Another variation on the acts_as_tree is the acts_as_sane_tree gem, which is configured for PostgreSQL 8.4 and comes with some cool extra methods (but isn’t nearly as massive as ancestry).
  • There are a lot of different ways to implement the acts_as_tree. Check out this railscast on tree-based navigation using this gem/plugin (beware the date on this one, though!).
  • There are a lot of different ways to deal with recursive data structures in Rails. Check out this in-depth look at the tried and tested options to learn more.