[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 | # Gemfile |
Then, run bundle install
.
1 | bundle install |
Configuration
-
Add has_closure_tree (or acts_as_tree, which is an alias of the same method) to your hierarchical model:
1
2
3class Tag < ActiveRecord::Base
has_closure_tree # or acts_as_tree
end -
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
5class AddParentIdToTag < ActiveRecord::Migration
def change
add_column :tags, :parent_id, :integer
end
endThe column must be nullable. Root nodes have a NULL parent_id.
-
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.
-
Run rake db:migrate
-
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 | grandparent = Tag.create(name: 'Grandparent') |
Then:
1 | grandparent.self_and_descendants.collect(&:name) |
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 | child = Tag.find_or_create_by_path(%w[grandparent parent child]) |
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 | d = Tag.find_or_create_by_path %w[a b c d] |
When it is more convenient to simply change the parent_id of a node directly (for example, when dealing with a form
1 | j = Tag.find 102 |
Nested hashes
hash_tree provides a method for rendering a subtree as an ordered nested hash:
1 | b = Tag.find_or_create_by_path %w(a b) |
If your tree is large (or might become so), use :limit_depth.
Without this option, hash_tree will load the entire contents of that table into RAM. Your server may not be happy trying to do this.
Eager loading
Since most of closure_tree’s methods (e.g. children) return regular ActiveRecord scopes, you can use the includes method for eager loading, e.g.
1 | comment.children.includes(:author) |
However, note that the above approach only eager loads the requested associations for the immediate children of comment. If you want to walk through the entire tree, you may still end up making many queries and loading duplicate copies of objects.
In some cases, a viable alternative is the following:
1 | comment.self_and_descendants.includes(:author) |
This would load authors for comment and all its descendants in a constant number of queries. However, the return value is an array of Comments, and the tree structure is thus lost, which makes it difficult to walk the tree using elegant recursive algorithms.
A third option is to use has_closure_tree_root on the model that is composed by the closure_tree model (e.g. a Post may be composed by a tree of Comments). So in post.rb, you would do:
1 | # app/models/post.rb |
This gives you a plain has_one association (root_comment) to the root Comment (i.e. that with null parent_id).
It also gives you a method called root_comment_including_tree, which you can invoke as follows:
1 | a_post.root_comment_including_tree(:author) |
The result of this call will be the root Comment with all descendants and associations loaded in a constant number of queries. Inverse associations are also setup on all nodes, so as you walk the tree, calling children or parent on any node will not trigger any further queries and no duplicate copies of objects are loaded into memory.
The class and foreign key of root_comment are assumed to be Comment and post_id, respectively. These can be overridden in the usual way.
The same caveat stated above with hash_tree also applies here: this method will load the entire tree into memory. If the tree is very large, this may be a bad idea, in which case using the eager loading methods above may be preferred.
More
Visit ClosureTree/closure_tree: Easily and efficiently make your ActiveRecord models support hierarchies - https://github.com/ClosureTree/closure_tree
to learn more about Available options, Accessing Data, FAQs, etc.
References
[2] closure_tree | RubyGems.org | your community gem host - https://rubygems.org/gems/closure_tree/