Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 139 lines (95 sloc) 8.044 kb
79671c4 @stefankroes Started on rdoc readme
authored
1 = Ancestry
1282d2e @stefankroes Initial commit
authored
2
3 Ancestry allows the records of a ActiveRecord model to be organised in a tree structure, using a single, intuitively formatted database column. 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 named_scopes, integrity checking, integrity restoration, arrangement of (sub)tree into hashes and different strategies for dealing with orphaned records.
4
79671c4 @stefankroes Started on rdoc readme
authored
5 = Installation
1282d2e @stefankroes Initial commit
authored
6
7 To apply Ancestry to any ActiveRecord model, follow these simple steps:
8
94aff79 @stefankroes Added .gitignore
authored
9 == Install Gem
10 * Add to config/environment.rb: config.gem 'ancestry'
11 * Install required gems: sudo rake gems:install
12 * Alternatively: sudo gem install ancestry
1282d2e @stefankroes Initial commit
authored
13
94aff79 @stefankroes Added .gitignore
authored
14 == Add Ancestry Column to Your Table
15 * Create migration: ./script/generate migration add_ancestry_to_[table] ancestry:string
16 * Add index to migration: add_index [table], :ancestry / remove_index [table], :ancestry
17 * Migrate your database: rake db:migrate
1282d2e @stefankroes Initial commit
authored
18
94aff79 @stefankroes Added .gitignore
authored
19 == Add Ancestry to Your Model
20 * Add to app/models/[model].rb: acts_as_tree
1282d2e @stefankroes Initial commit
authored
21
22 Your model is now a tree!
23
79671c4 @stefankroes Started on rdoc readme
authored
24 = Organising Records Into A Tree
1282d2e @stefankroes Initial commit
authored
25
26 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:
27
28 TreeNode.create!(:name => 'Stinky', :parent => TreeNode.create!(:name => 'Squeeky'))
29
79671c4 @stefankroes Started on rdoc readme
authored
30 = Navigating Your Tree
1282d2e @stefankroes Initial commit
authored
31
32 To navigate an Ancestry model, use the following methods on any instance / record:
33
94aff79 @stefankroes Added .gitignore
authored
34 [parent] Returns the parent of the record
35 [root] Returns the root of the tree the record is in
36 [root_id] Returns the id of the root of the tree the record is in
37 [is_root?] Returns true if the record is a root node, false otherwise
38 [ancestor_ids] Returns a list of ancestor ids, starting with the root id and ending with the parent id
39 [ancestors] Scopes the model on ancestors of the record
40 [path_ids] Returns a list the path ids, starting with the root is and ending with the node's own id
41 [path] Scopes model on path records of the record
42 [children] Scopes the model on children of the record
43 [child_ids] Returns a list of child ids
44 [has_children?] Returns true if the record has any children, false otherwise
45 [is_childless?] Returns true is the record has no childen, false otherwise
46 [siblings] Scopes the model on siblings of the record, the record itself is included
47 [sibling_ids] Returns a list of sibling ids
48 [has_siblings?] Returns true if the record's parent has more than one child
49 [is_only_child?] Returns true if the record is the only child of its parent
50 [descendants] Scopes the model on direct and indirect children of the record
51 [descendant_ids] Returns a list of a descendant ids
52 [subtree] Scopes the model on descendants and itself
53 [subtree_ids] Returns a list of all ids in the record's subtree
1282d2e @stefankroes Initial commit
authored
54
79671c4 @stefankroes Started on rdoc readme
authored
55 = Scopes
1282d2e @stefankroes Initial commit
authored
56
57 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:
58
59 node.children.exists?(:name => 'Mary')
60 node.subtree.all(:order => :name, :limit => 10).each do; ...; end
61 node.descendants.count
62
79671c4 @stefankroes Started on rdoc readme
authored
63 = Named Scopes
1282d2e @stefankroes Initial commit
authored
64
65 For convenience, a couple of named scopes are included as class level:
66
94aff79 @stefankroes Added .gitignore
authored
67 [root] Only root nodes
68 ancestor_of([node)] Only ancestors of node, node can be either a record or an id
69 child_of([node)] Only children of node, node can be either a record or an id
70 [descendant_of(node)] Only descendants of node, node can be either a record or an id
71 [sibling_of(node)] Only siblings of node, node can be either a record or an id
1282d2e @stefankroes Initial commit
authored
72
79671c4 @stefankroes Started on rdoc readme
authored
73 = acts_as_tree Options
1282d2e @stefankroes Initial commit
authored
74
75 The acts_as_tree methods supports two options:
76
94aff79 @stefankroes Added .gitignore
authored
77 [ancestry_column] Pass in a symbol to instruct Ancestry to use a different column name to store record ancestry
78 [orphan_strategy] Instruct Ancestry what to do with children of a node that is destroyed:
79 :destroy All children are destroyed as well (default)
80 :rootify The children of the destroyed node become root nodes
81 :restrict An AncestryException is raised if any children exist
1282d2e @stefankroes Initial commit
authored
82
79671c4 @stefankroes Started on rdoc readme
authored
83 = Arrangement
1282d2e @stefankroes Initial commit
authored
84
85 Ancestry can arrange an entire subtree into nested hashes for easy navigation after retrieval from the database. TreeNode.arrange could for example return:
86
87 { #<TreeNode id: 100018, name: "Stinky", ancestry: nil>
88 => { #<TreeNode id: 100019, name: "Crunchy", ancestry: "100018">
89 => { #<TreeNode id: 100020, name: "Squeeky", ancestry: "100018/100019">
90 => {}
91 }
92 }
93 }
94
95 The arrange method also works on a scoped class, for example:
96
97 TreeNode.find_by_name('Crunchy').subtree.arrange
98
79671c4 @stefankroes Started on rdoc readme
authored
99 = Integrity Checking and Restoration
1282d2e @stefankroes Initial commit
authored
100
101 I currently don't see any way Ancestry tree integrity could get compromised without explicitly setting cyclic parents but I included code 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. To restore integrity use: [Model].restore_ancestry_integrity.
102
103 For example, from IRB:
104
105 >> stinky = TreeNode.create :name => 'Stinky'
94aff79 @stefankroes Added .gitignore
authored
106 $ #<TreeNode id: 1, name: "Stinky", ancestry: nil>
1282d2e @stefankroes Initial commit
authored
107 >> squeeky = TreeNode.create :name => 'Squeeky', :parent => stinky
94aff79 @stefankroes Added .gitignore
authored
108 $ #<TreeNode id: 2, name: "Squeeky", ancestry: "1">
1282d2e @stefankroes Initial commit
authored
109 >> stinky.update_attribute :parent, squeeky
94aff79 @stefankroes Added .gitignore
authored
110 $ true
1282d2e @stefankroes Initial commit
authored
111 >> TreeNode.all
94aff79 @stefankroes Added .gitignore
authored
112 $ [#<TreeNode id: 1, name: "Stinky", ancestry: "1/2">, #<TreeNode id: 2, name: "Squeeky", ancestry: "1/2/1">]
1282d2e @stefankroes Initial commit
authored
113 >> TreeNode.check_ancestry_integrity
114 !! Ancestry::AncestryIntegrityException: Conflicting parent id in node 1: 2 for node 1, expecting nil
115 >> TreeNode.restore_ancestry_integrity
94aff79 @stefankroes Added .gitignore
authored
116 $ [#<TreeNode id: 1, name: "Stinky", ancestry: 2>, #<TreeNode id: 2, name: "Squeeky", ancestry: nil>]
1282d2e @stefankroes Initial commit
authored
117
79671c4 @stefankroes Started on rdoc readme
authored
118 = Testing
1282d2e @stefankroes Initial commit
authored
119
120 The Ancestry gem comes with a unit test suite consisting of about 1400 assertions in 18 tests. It takes about 4 seconds to run on sqlite. To run it yourself, install Ancestry as a plugin, go to the ancestry folder and type 'rake'. The test suite is located in 'test/acts_as_tree_test.rb'.
121
79671c4 @stefankroes Started on rdoc readme
authored
122 = Internals
1282d2e @stefankroes Initial commit
authored
123
124 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.
125
126 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 a index cannot be put on the column in that case.
127
79671c4 @stefankroes Started on rdoc readme
authored
128 = Future Work
1282d2e @stefankroes Initial commit
authored
129
130 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 and only as I see fit. Something that definitely needs to be added in the future is constraints on depth, something like: tree_node.subtree.to_depth(4)
131
79671c4 @stefankroes Started on rdoc readme
authored
132 = Feedback
1282d2e @stefankroes Initial commit
authored
133
134 For questions, bug reports and feature requests, please contact me at s.a.kroes[at]gmail.com
135
136
137
138 Copyright (c) 2009 Stefan Kroes, released under the MIT license
Something went wrong with that request. Please try again.