DataMapper plugin enabling easy creation of tree structures from your DM models.
This requires a foreign key property for your model, which by default would be called :parent_id
.
Install the dm-is-tree
gem.
$ (sudo)? gem install dm-is-tree
Download or clone dm-is-versioned
from Github.
$ cd /path/to/dm-is-tree $ rake install # will install dm-is-tree
To start using this gem, just…
require 'dm-is-tree'
Lets say we have a Category model…
class Category include DataMapper::Resource property :id, Serial property :name, String end
…and we want to have a tree structure within it, something like this:
the_parent +- child +- grandchild1 +- grandchild2
To achieve this we just add the following to the model:
class Category <snip...> is :tree, :order => :name end # No need to define the :parent_id property, it will be added automatically property :parent_id, Integer
This will automatically add the following to your model:
-
#parent
/#parent=
-
#children
/#children=
-
#siblings
-
#generation
-
#ancestors
-
also aliased as
#self_and_siblings
for those used to AR’s :acts_as_tree
-
-
#root
-
also aliased as
#first_root
for those used to AR’s :acts_as_tree
-
-
#first_root
-
#roots
Before we go onto the usage examples, a few quick words about configuration options available:
Specifies the column name to use for tracking of the tree (default: #parent_id
).
class Category <snip...> is :tree, :child_key => :some_other_foreign_key_id end
Specifies the name of the Model to use for the tree (default: Model class name defined in)
class Category <snip...> is :tree, :model => 'SomeStrangeModelName' end
Specifies the sort order of the children when retrieving them (default: not present)
class Category <snip...> is :tree, :order => [:updated_at, :name] end
To create the above structure, we would start with:
the_parent = Category.create(:name => "the_parent")
The #parent
instance method returns the node referenced by the foreign key - :parent_id
or the defined :child_key
.
# by default #parent and #parent_id return nil when there is no parent the_parent.parent # => nil
The #parent=
instance method sets the :parent_id
foreign key to the parent’s id attribute value.
To define a parent you can use either of these syntaxes:
a_child = Category.create(:name => "a_child", :parent => the_parent) a_child = Category.create(:name => "a_child", :parent_id => the_parent.id) a_child = Category.create(:name => "a_child") a_child.parent = the_parent
When retrieving the parent, you will receive the full parent object ( or nil
if none was declared )
a_child.parent # => the_parent
The #children
instance method returns all nodes with the current node as their parent, in the order specified by the :order
configuration option.
# by default #children return an empty Array when there are no children a_child.children # => [] # or an Array with child objects when there are children the_parent.children # => [a_child]
The #children=
instance method adds children by setting the :parent_id
foreign key to the parent’s id attribute value.
To add a child you can use either of these syntaxes:
child = the_parent.children.create(:name => "child") grandchild1 = Category.create(:name => "grandchild1") child.children << grandchild1 grandchild2 = child.children.create(:name => "grandchild2")
When retrieving children, or a child, you will receive an Array of child objects.
child.children # => [grandchild1, grandchild2,...] # just retrieve the first child child.children.first # => grandchild1
The #siblings
instance method returns all the children of the parent, excluding the current node.
# by default #siblings return an empty Array when there are no siblings the_parent.siblings # => [] grandchild1.siblings # => [grandchild2]
The #generation
instance method returns all the children of the parent, including the current node.
# by default #generation return an Array with itself only when it's a 'lonely child' the_parent.generation # => [the_parent] # grandchild1.generation # => [grandchild1, grandchild2]
The #ancestors
instance method returns all the ancestors of the current node.
# by default it returns an empty Array when born through immaculate conception (is root) the_parent.ancestors # => [] grandchild2.ancestors # => [the_parent, a_child]
The #root
instance method returns the root (parent) of the current node.
# by default returns itself only when it's the root node the_parent.root # => the_parent a_child.root # => the_parent grandchild2.root # => the_parent
The #first_root
class method returns the first root declared in the model.
Category.first_root # => the_parent
The #roots
class method returns an Array of the roots declared in the model.
Category.roots # => [the_parent] parent2 = Category.create(:name => 'parent2') Category.roots # => [the_parent, parent2]
parent = Category.create(:name => "parent") child = parent.children.create(:name => "child") grandchild1 = child.children.create(:name => "grandchild1") grandchild2 = Category.create(:name => "grandchild2") child.children << grandchild2 grandchild3 = Category.create(:name => "grandchild2") grandchild3.parent = child parent.parent # => nil child.parent # => parent parent.children # => [child] parent.children.first.children.first # => grandchild1 parent.siblings # => [] grandchild1.siblings # => [grandchild2] parent.generation # => [parent] grandchild1.generation # => [grandchild1, grandchild2] parent.ancestors # => [] grandchild2.ancestors # => [parent, child] parent.root # => parent parent.root # => parent grandchild2.root # => parent Category.first_root # => parent Category.roots # => [parent]
Now there are some gotcha’s that might not be entirely obvious to everyone, so let’s clarify them here.
By default dm-is-tree allows you to save a record as a child of it self, which is quite unnatural. To prevent this, I would humbly suggest adding this custom validation code to your model(s).
class Category <snip...> # prevent saving Category as child of self, except when new? validates_with_method :parent_id, :method => :category_cannot_be_made_a_child_of_self, :unless => :new? protected def category_cannot_be_made_a_child_of_self if self.id === self.parent_id return [ false, "A Category [ #{self.name} ] cannot be made a child of it self [ #{self.name} ]" ] else return true end end end
An example:
parent = Category.create(:name => "parent") child = parent.children.create(:name => 'child') child.parent = child child.save # => return false child.errors.on(:parent_id) # => ["A Category [ child ] cannot be made a child of it self [ child ]"]
By default the sorting order is alphabetic, but this spans the entire table (with all nodes), which might not be what you want.
To prevent this, order the results by :parent_id
first, and secondly by :name
or whatever you wish to sort by.
class Category <snip...> is :tree, :order => [:parent_id, :name] end
That’s about it.
If something is not behaving intuitively, it is a bug, and should be reported. Report it here: datamapper.lighthouseapp.com/
-
Make it automatically prevent saving self as child of self.
-
Anything else missing?
-
Fork the project.
-
Make your feature addition or bug fix.
-
Add tests for it. This is important so we don’t break it in a future version unintentionally.
-
Commit, do not mess with rakefile, version, or history.
-
(if you want to have your own version, that is fine but bump version in a commit by itself we can ignore when we pull)
-
-
Send us a pull request. Bonus points for topic branches.
Copyright © 2011 Timothy Bennett. Released under the MIT License.
See LICENSE for details.
Credit also goes to these contributors.
Current Maintainer: Garrett Heaver (www.linkedin.com/pub/dir/garrett/heaver)