A Post Entitled getting into ancestry
Heading into The Big Rewrite I finally took the opportunity to play with the ancestry gem. Initially developed as a replacement for for acts_as_tree, ancestry has evolved to something much more akin to nested-set support in a tree-like structure. The advances allow you to pluck out a entire subtree or hierarchy path in a single trip to the database. Awesome.
The one thing that I wish we had for ancestry is a clear upgrade path for those of us who discovered ancestry after building up an application with acts_as_tree. Stefan has kindly renamed the initializing ‘acts_as_tree’ method to ‘has_ancestry’ which is a step in the right direction. Hopefully it will bother me enough to actually do something about it…
The ancestry gem has greatly simplified a tangential issue in the code base. My first use of ancestry was in my Account model. In this particular application Account is hierarchical in order to allow higher-level accounts to establish rules that must be followed by lower-level accounts. Specifically, the corporation (root) will set rules for everyone, the regional manager (child of corporate) may extend those rules, and the local account (leaf of the tree) is subject to both of them.
With acts_as_tree the solution to this was not difficult but it was time consuming. First, we created a method to walk up the hierarchy and collect the parent nodes of the tree until we hit the root node. Each parent node is unshifted into the array so that the resulting array index corresponds to the depth of the node in the hierarchy (index 0 is the root node, etc). Another method can then iterate over that hierarchy and collect the rules into a single array as shown below.
class Account < ActiveRecord::Base
has_ancestry
...
def account_hierarchy
hierarchy = [self]
current_account = parent
while(!current_account.nil?) do
hierarchy.unshift current_account
current_account = current_account.parent
end
return hierarchy
end
def collect_from_hierarchy(collection_name)
account_hierarchy.collect{|acct| acct.send(collection_name) }.flatten.compact
end
end
While straight-forward, this code clearly will not scale all that well. We will have 2N database cycles (N=node depth) every time we need to retrieve a collection of objects. Yuck.
The ancestry gem dramatically reduces this. The gem provides a method called ‘path’ (and corresponding ‘path_ids’) that returns a set roughly equivalent to the ‘account_hierarchy’ method shown above. The win is not that we no longer need the ‘account_heirarchy’ method (obviously the equivalent method exists somewhere) but that ancestry manages to build the path collection in one trip to the database.
That bit of inspiration led to a refactoring of the ‘collect_from_hierarchy’ method. Since we have the ids of the accounts it was obvious that we could retrieve the collection of rules in just one trip as well.
class Account < ActiveRecord::Base
has_ancestry
...
def collect_from_hierarchy(collection_name)
Object.const_get(collection_name.to_s.classify).where :account_id => path_ids
end
end
In this refactoring we are taking advantage of the fact that Ruby classes are constants. We accept the name of the collection we would like to assemble from the hierarchy, convert to a string (allowing the user to call it with a symbol), and then get the class name equivalent by calling classify and finally grab a handle to the class by asking Ruby to retrieve the class from the known constants. We then ask the class to return the collection of rules where the account_id for the rule is one of the members of the path_ids. As an added bonus, the refactoring also takes advantage of scopes so that the caller may chain additional scopes as necessary.
acct.collect_from_hierarchy(:birthdays).order(:born_on)
acct.collect_from_hierarchy(:salaries).order(:amount).paginate :page => params[:page]
Admittedly, this refactoring was available earlier. The ‘account_hierarchy’ method could have been written to return an array of ids rather than the objects themselves. ancestry provided the inspiration by making that fact obvious. We stand on the shoulders of giants, indeed.
Loading...