README.rdoc

Ancestry

Ancestry is a gem/plugin that allows the records of a Ruby on Rails
ActiveRecord model to be organised as a tree structure (or hierarchy). It
uses a single, intuitively formatted database column, using a variation on
the materialised path pattern. It exposes all the standard tree structure
relations (ancestors, parent, root, children, siblings, descendants) and
all of them can be fetched in a single SQL query. Additional features are
STI support, scopes, depth caching, depth constraints, easy migration from
older plugins/gems, integrity checking, integrity restoration, arrangement
of (sub)tree into hashes and different strategies for dealing with orphaned
records.

Installation

To apply Ancestry to any ActiveRecord model, follow these simple steps:

Using acts_as_tree instead of has_ancestry

In version 1.2.0 the acts_as_tree method was renamed to
has_ancestry in order to allow usage of both the acts_as_tree gem and
the ancestry gem in a single application. To not break backwards
compatibility, the has_ancestry method is aliased with acts_as_tree if
ActiveRecord::Base does not respond to acts_as_tree. acts_as_tree will
continue to be supported in the future as I personally prefer it.

Organising records into a tree

You can use the parent attribute to organise your records into a tree. If
you have the id of the record you want to use as a parent and don't
want to fetch it, you can also use parent_id. Like any virtual model
attributes, parent and parent_id can be set using parent= and parent_id= on
a record or by including them in the hash passed to new, create, create!,
update_attributes and update_attributes!. For example:

Navigating your tree

To navigate an Ancestry model, use the following methods on any instance /
record:

parent Returns the parent of the record, nil for a root node
parent_id Returns the id of the parent of the record, nil for a root node
root Returns the root of the tree the record is in, self for a root node
root_id Returns the id of the root of the tree the record is in
is_root? Returns true if the record is a root node, false otherwise
ancestor_ids Returns a list of ancestor ids, starting with the root id and ending with the parent id
ancestors Scopes the model on ancestors of the record
path_ids Returns a list the path ids, starting with the root id and ending with the node's own id
path Scopes model on path records of the record
children Scopes the model on children of the record
child_ids Returns a list of child ids
has_children? Returns true if the record has any children, false otherwise
is_childless? Returns true is the record has no childen, false otherwise
siblings Scopes the model on siblings of the record, the record itself is included
sibling_ids Returns a list of sibling ids
has_siblings? Returns true if the record's parent has more than one child
is_only_child? Returns true if the record is the only child of its parent
descendants Scopes the model on direct and indirect children of the record
descendant_ids Returns a list of a descendant ids
subtree Scopes the model on descendants and itself
subtree_ids Returns a list of all ids in the record's subtree
depth Return the depth of the node, root nodes are at depth 0

Options for has_ancestry

The has_ancestry methods supports the following options:

:ancestry_column Pass in a symbol to store ancestry in a different column
:orphan_strategy Instruct Ancestry what to do with children of a node that is destroyed:
:destroy All children are destroyed as well (default)
:rootify The children of the destroyed node become root nodes
:restrict An AncestryException is raised if any children exist
:adopt The orphan subtree is added to the parent of the deleted node.If the deleted node is Root, then rootify the orphan subtree.
:cache_depth Cache the depth of each node in the 'ancestry_depth' column (default: false)
If you turn depth_caching on for an existing model:
- Migrate: add_column [table], :ancestry_depth, :integer, :default => 0
- Build cache: TreeNode.rebuild_depth_cache!
:depth_cache_column Pass in a symbol to store depth cache in a different column
:primary_key_format Supply a regular expression that matches the format of your primary key.
By default, primary keys only match integers ([0-9]+).

(Named) Scopes

Where possible, the navigation methods return scopes instead of records,
this means additional ordering, conditions, limits, etc. can be applied and
that the result can be either retrieved, counted or checked for existence.
For example:

For convenience, a couple of named scopes are included at the class level:

roots Root nodes
ancestors_of(node) Ancestors of node, node can be either a record or an id
children_of(node) Children of node, node can be either a record or an id
descendants_of(node) Descendants of node, node can be either a record or an id
subtree_of(node) Subtree of node, node can be either a record or an id
siblings_of(node) Siblings of node, node can be either a record or an id

Thanks to some convenient rails magic, it is even possible to create nodes
through the children and siblings scopes:

Please note that depth constraints cannot be passed to ancestor_ids and
path_ids. The reason for this is that both these relations can be fetched
directly from the ancestry column without performing a database query. It
would require an entirely different method of applying the depth
constraints which isn't worth the effort of implementing. You can use
ancestors(depth_options).map(&:id) or
ancestor_ids.slice(min_depth..max_depth) instead.

STI support

Ancestry works fine with STI. Just create a STI inheritance hierarchy and
build an Ancestry tree from the different classes/models. All Ancestry
relations that where described above will return nodes of any model type.
If you do only want nodes of a specific subclass you'll have to add a
condition on type for that.

Arrangement

Ancestry can arrange an entire subtree into nested hashes for easy
navigation after retrieval from the database. TreeNode.arrange could for
example return:

The arrange method takes ActiveRecord find options. If you want your hashes
to be ordered, you should pass the order to the arrange method instead of
to the scope. This also works for Ruby 1.8 since an OrderedHash is
returned. For example:

TreeNode.find_by_name('Crunchy').subtree.arrange(:order => :name)

Sorting

If you just want to sort an array of nodes as if you were traversing them
in preorder, you can use the sort_by_ancestry class method:

TreeNode.sort_by_ancestry(array_of_nodes)

Note that since materialised path trees don't support ordering within a
rank, the order of siblings depends on their order in the original array.

Migrating from plugin that uses parent_id column

Most current tree plugins use a parent_id column (has_ancestry,
awesome_nested_set, better_nested_set, acts_as_nested_set). With ancestry
its easy to migrate from any of these plugins, to do so, use the
build_ancestry_from_parent_ids! method on your ancestry model. These steps
provide a more detailed explanation:

Integrity checking and restoration

I don't see any way Ancestry tree integrity could get compromised
without explicitly setting cyclic parents or invalid ancestry and
circumventing validation with update_attribute, if you do, please let me
know.

Ancestry includes some methods for detecting integrity problems and
restoring integrity just to be sure. To check integrity use:
[Model].check_ancestry_integrity!. An AncestryIntegrityException will be
raised if there are any problems. You can also specify :report => :list
to return an array of exceptions or :report => :echo to echo any error
messages. To restore integrity use: [Model].restore_ancestry_integrity!.

Tests

The Ancestry gem comes with a unit test suite consisting of about 1800
assertions in about 30 tests. It takes about 10 seconds to run on sqlite.
To run it yourself check out the repository from GitHub, copy
test/database.example.yml to test/database.yml and type 'rake'. You
can pass rake style options for ActiveRecord version to test against (e.g.
ar=3.0.1) and database to test against (e.g. db=mysql). The test suite is
located in test/has_ancestry_test.rb.

Internals

As can be seen in the previous section, Ancestry stores a path from the
root to the parent for every node. This is a variation on the materialised
path database pattern. It allows Ancestry to fetch any relation (siblings,
descendants, etc.) in a single SQL query without the complicated algorithms
and incomprehensibility associated with left and right values.
Additionally, any inserts, deletes and updates only affect nodes within the
affected node's own subtree.

In the example above, the ancestry column is created as a string. This puts
a limitation on the depth of the tree of about 40 or 50 levels, which I
think may be enough for most users. To increase the maximum depth of the
tree, increase the size of the string that is being used or change it to a
text to remove the limitation entirely. Changing it to a text will however
decrease performance because an index cannot be put on the column in that
case.

The materialised path pattern requires Ancestry to use a 'like'
condition in order to fetch descendants. This should not be particularly
slow however since the the condition never starts with a wildcard which
allows the DBMS to use the column index. If you have any data on
performance with a large number of records, please drop me line.

Version history

The latest version of ancestry is recommended. The three numbers of each
version numbers are respectively the major, minor and patch versions. We
started with major version 1 because it looks so much better and ancestry
was already quite mature and complete when it was published. The major
version is only bumped when backwards compatibility is broken. The minor
version is bumped when new features are added. The patch version is bumped
when bugs are fixed.

Thanks to a patch from tom taylor, Ancestry now works with different
primary keys

Version 1.1.3 (2009-11-01)

Fixed a pretty bad bug where several operations took far too many queries

Version 1.1.2 (2009-10-29)

Added validation for depth cache column

Added STI support (reported broken)

Version 1.1.1 (2009-10-28)

Fixed some parentheses warnings that where reported

Fixed a reported issue with arrangement

Fixed issues with ancestors and path order on postgres

Added ordered_by_ancestry scope (needed to fix issues)

Version 1.1.0 (2009-10-22)

Depth caching (and cache rebuilding)

Depth method for nodes

Named scopes for selecting by depth

Relative depth options for tree navigation methods:

ancestors

path

descendants

descendant_ids

subtree

subtree_ids

Updated README

Easy migration from existing plugins/gems

acts_as_tree checks unknown options

acts_as_tree checks that options are hash

Added a bang (!) to the integrity functions

Since these functions should only be used from ./script/console and not
from your application, this change is not considered as breaking backwards
compatibility and the major version wasn't bumped.

Updated install script to point to documentation

Removed rails specific init

Removed uninstall script

Version 1.0.0 (2009-10-16)

Initial version

Tree building

Tree navigation

Integrity checking / restoration

Arrangement

Orphan strategies

Subtree movement

Named scopes

Validations

Future work

I will try to keep Ancestry up to date with changing versions of Rails and
Ruby and also with any bug reports I might receive. I will implement new
features on request as I see fit. One thing I definitely want to do soon is
some proper performance testing.