[Awesome Ruby Gem] Use closure_tree gem to model hierarchical tree data structure

closure_tree

Closure_tree lets your ActiveRecord models act as nodes in a tree data structure.

Common applications include modeling hierarchical data, like tags, threaded comments, page graphs in CMSes, and tracking user referrals.

Installation

You can install it as a gem:

1
$ gem install closure_tree

or add it into a Gemfile (Bundler):

1
2
3
4
5
# Gemfile

# ClosureTree/closure_tree: Easily and efficiently make your ActiveRecord models support hierarchies
# https://github.com/ClosureTree/closure_tree
gem 'closure_tree', `7.2.0'

Then, run bundle install.

1
$ bundle install

Configuration

  1. Add has_closure_tree (or acts_as_tree, which is an alias of the same method) to your hierarchical model:

    1
    2
    3
    class Tag < ActiveRecord::Base
    has_closure_tree # or acts_as_tree
    end
  2. Add a migration to add a parent_id column to the hierarchical model. You may want to also add a column for deterministic ordering of children, but that’s optional.

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

    The column must be nullable. Root nodes have a NULL parent_id.

  3. Run rails g closure_tree:migration tag (and replace tag with your model name) to create the closure tree table for your model.

    By default the table name will be the model’s table name, followed by “_hierarchies”. Note that by calling has_closure_tree, a “virtual model” (in this case, TagHierarchy) will be created dynamically. You don’t need to create it.

  4. Run rake db:migrate

  5. If you’re migrating from another system where your model already has a parent_id column, run Tag.rebuild! and your tag_hierarchies table will be truncated and rebuilt.

    If you’re starting from scratch you don’t need to call rebuild!.

NOTE: Run rails g closure_tree:config to create an initializer with extra configurations. (Optional)

Usage

Creation

Create a root node:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
grandparent = Tag.create(name: 'Grandparent')

# Child nodes are created by appending to the children collection:

parent = grandparent.children.create(name: 'Parent')

#Or by appending to the children collection:

child2 = Tag.new(name: 'Second Child')
parent.children << child2

# Or by calling the "add_child" method:

child3 = Tag.new(name: 'Third Child')
parent.add_child child3

# Or by setting the parent on the child :

Tag.create(name: 'Fourth Child', parent: parent)

Then:

1
2
3
4
5
grandparent.self_and_descendants.collect(&:name)
=> ["Grandparent", "Parent", "First Child", "Second Child", "Third Child", "Fourth Child"]

child1.ancestry_path
=> ["Grandparent", "Parent", "First Child"]

find_or_create_by_path

You can find as well as find_or_create by “ancestry paths”.

If you provide an array of strings to these methods, they reference the name column in your model, which can be overridden with the :name_column option provided to has_closure_tree.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
child = Tag.find_or_create_by_path(%w[grandparent parent child])

# As of v5.0.0, find_or_create_by_path can also take an array of attribute hashes:

child = Tag.find_or_create_by_path([
{name: 'Grandparent', title: 'Sr.'},
{name: 'Parent', title: 'Mrs.'},
{name: 'Child', title: 'Jr.'}
])

# If you're using STI, The attribute hashes can contain the sti_name and things work as expected:

child = Label.find_or_create_by_path([
{type: 'DateLabel', name: '2014'},
{type: 'DateLabel', name: 'August'},
{type: 'DateLabel', name: '5'},
{type: 'EventLabel', name: 'Visit the Getty Center'}
])

Moving nodes around the tree

Nodes can be moved around to other parents, and closure_tree moves the node’s descendancy to the new parent for you:

1
2
3
4
5
6
d = Tag.find_or_create_by_path %w[a b c d]
h = Tag.find_or_create_by_path %w[e f g h]
e = h.root
d.add_child(e) # "d.children << e" would work too, of course
h.ancestry_path
=> ["a", "b", "c", "d", "e", "f", "g", "h"]

When it is more convenient to simply change the parent_id of a node directly (for example, when dealing with a form