Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 296 lines (222 sloc) 17.977 kB
79671c4 @stefankroes Started on rdoc readme
authored
1 = Ancestry
1282d2e @stefankroes Initial commit
authored
2
f1955bb @stefankroes Changed test suite to run independently of Rails application
authored
3 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.
1282d2e @stefankroes Initial commit
authored
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
22a1984 - Added some tests for node creation through scopes
Stefan Kroes authored
9 1. Install gem
90c4a10 @stefankroes Updated README
authored
10 - Install gemcutter gem: <b>sudo gem install gemcutter</b> (maybe you need: gem update --system)
11 - Add gemcutter.org as default gem source: <b>gem tumble</b>
12 - Add to config/environment.rb: <b>config.gem 'ancestry'</b>
13 - Install required gems: <b>sudo rake gems:install</b>
22a1984 - Added some tests for node creation through scopes
Stefan Kroes authored
14 - Alternatively: sudo gem install ancestry
15 - If you don't want gemcutter: config.gem 'ancestry', :source => 'gemcutter.org'
16 - Alternatively: sudo gem install ancestry --source gemcutter.org
1282d2e @stefankroes Initial commit
authored
17
22a1984 - Added some tests for node creation through scopes
Stefan Kroes authored
18 2. Add ancestry column to your table
90c4a10 @stefankroes Updated README
authored
19 - Create migration: <b>./script/generate migration add_ancestry_to_[table] ancestry:string</b>
20 - Add index to migration: <b>add_index [table], :ancestry</b> (UP) / <b>remove_index [table], :ancestry</b> (DOWN)
21 - Migrate your database: <b>rake db:migrate</b>
1282d2e @stefankroes Initial commit
authored
22
22a1984 - Added some tests for node creation through scopes
Stefan Kroes authored
23 3. Add ancestry to your model
ecea76f @stefankroes Version 1.2.0
authored
24 - Add to app/models/[model].rb: <b>has_ancestry</b>
1282d2e @stefankroes Initial commit
authored
25
26 Your model is now a tree!
27
ecea76f @stefankroes Version 1.2.0
authored
28 = Using acts_as_tree instead of has_ancestry
29
30 In version 1.2.0 the <b>acts_as_tree</b> method was <b>renamed to has_ancestry</b> 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.
31
41c344d @stefankroes Updated readme
authored
32 = Organising records into a tree
1282d2e @stefankroes Initial commit
authored
33
34 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:
35
95e6701 @stefankroes Updated README
authored
36 TreeNode.create! :name => 'Stinky', :parent => TreeNode.create!(:name => 'Squeeky')
1282d2e @stefankroes Initial commit
authored
37
48d00be @stefankroes Updated readme
authored
38 You can also create children through the children relation on a node:
39
95e6701 @stefankroes Updated README
authored
40 node.children.create :name => 'Stinky'
48d00be @stefankroes Updated readme
authored
41
41c344d @stefankroes Updated readme
authored
42 = Navigating your tree
1282d2e @stefankroes Initial commit
authored
43
44 To navigate an Ancestry model, use the following methods on any instance / record:
45
df705c3 @stefankroes Version 1.1.0 done!
authored
46 parent Returns the parent of the record, nil for a root node
47 parent_id Returns the id of the parent of the record, nil for a root node
48 root Returns the root of the tree the record is in, self for a root node
49 root_id Returns the id of the root of the tree the record is in
50 is_root? Returns true if the record is a root node, false otherwise
51 ancestor_ids Returns a list of ancestor ids, starting with the root id and ending with the parent id
52 ancestors Scopes the model on ancestors of the record
22c43f4 @stefankroes Fixed to typos and changed date in gemspec
authored
53 path_ids Returns a list the path ids, starting with the root id and ending with the node's own id
df705c3 @stefankroes Version 1.1.0 done!
authored
54 path Scopes model on path records of the record
55 children Scopes the model on children of the record
56 child_ids Returns a list of child ids
57 has_children? Returns true if the record has any children, false otherwise
58 is_childless? Returns true is the record has no childen, false otherwise
59 siblings Scopes the model on siblings of the record, the record itself is included
60 sibling_ids Returns a list of sibling ids
61 has_siblings? Returns true if the record's parent has more than one child
62 is_only_child? Returns true if the record is the only child of its parent
63 descendants Scopes the model on direct and indirect children of the record
64 descendant_ids Returns a list of a descendant ids
65 subtree Scopes the model on descendants and itself
66 subtree_ids Returns a list of all ids in the record's subtree
67 depth Return the depth of the node, root nodes are at depth 0
68
ecea76f @stefankroes Version 1.2.0
authored
69 = Options for has_ancestry
df705c3 @stefankroes Version 1.1.0 done!
authored
70
ecea76f @stefankroes Version 1.2.0
authored
71 The has_ancestry methods supports the following options:
df705c3 @stefankroes Version 1.1.0 done!
authored
72
73 :ancestry_column Pass in a symbol to store ancestry in a different column
74 :orphan_strategy Instruct Ancestry what to do with children of a node that is destroyed:
75 :destroy All children are destroyed as well (default)
76 :rootify The children of the destroyed node become root nodes
77 :restrict An AncestryException is raised if any children exist
78 :cache_depth Cache the depth of each node in the 'ancestry_depth' column (default: false)
e3ea26a @stefankroes - Version 1.1.2 (2009-10-29)
authored
79 If you turn depth_caching on for an existing model:
ad6dda9 @dburrows amended cache depth migration
dburrows authored
80 - Migrate: add_column [table], :ancestry_depth, :integer, :default => 0
e3ea26a @stefankroes - Version 1.1.2 (2009-10-29)
authored
81 - Build cache: TreeNode.rebuild_depth_cache!
df705c3 @stefankroes Version 1.1.0 done!
authored
82 :depth_cache_column Pass in a symbol to store depth cache in a different column
1282d2e @stefankroes Initial commit
authored
83
22a1984 - Added some tests for node creation through scopes
Stefan Kroes authored
84 = (Named) Scopes
1282d2e @stefankroes Initial commit
authored
85
86 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:
87
8d2cffb @stefankroes Fixed some problems in the README
authored
88 node.children.exists?(:name => 'Mary')
89 node.subtree.all(:order => :name, :limit => 10).each do; ...; end
90 node.descendants.count
1282d2e @stefankroes Initial commit
authored
91
22a1984 - Added some tests for node creation through scopes
Stefan Kroes authored
92 For convenience, a couple of named scopes are included at the class level:
1282d2e @stefankroes Initial commit
authored
93
df705c3 @stefankroes Version 1.1.0 done!
authored
94 roots Root nodes
95 ancestors_of(node) Ancestors of node, node can be either a record or an id
96 children_of(node) Children of node, node can be either a record or an id
97 descendants_of(node) Descendants of node, node can be either a record or an id
ecea76f @stefankroes Version 1.2.0
authored
98 subtree_of(node) Subtree of node, node can be either a record or an id
df705c3 @stefankroes Version 1.1.0 done!
authored
99 siblings_of(node) Siblings of node, node can be either a record or an id
1282d2e @stefankroes Initial commit
authored
100
22a1984 - Added some tests for node creation through scopes
Stefan Kroes authored
101 Thanks to some convenient rails magic, it is even possible to create nodes through the children and siblings scopes:
102
48d00be @stefankroes Updated readme
authored
103 node.children.create
104 node.siblings.create!
105 TestNode.children_of(node_id).new
106 TestNode.siblings_of(node_id).create
1282d2e @stefankroes Initial commit
authored
107
df705c3 @stefankroes Version 1.1.0 done!
authored
108 = Selecting nodes by depth
1282d2e @stefankroes Initial commit
authored
109
ecea76f @stefankroes Version 1.2.0
authored
110 When depth caching is enabled (see has_ancestry options), five more named scopes can be used to select nodes on their depth:
1282d2e @stefankroes Initial commit
authored
111
df705c3 @stefankroes Version 1.1.0 done!
authored
112 before_depth(depth) Return nodes that are less deep than depth (node.depth < depth)
113 to_depth(depth) Return nodes up to a certain depth (node.depth <= depth)
114 at_depth(depth) Return nodes that are at depth (node.depth == depth)
115 from_depth(depth) Return nodes starting from a certain depth (node.depth >= depth)
116 after_depth(depth) Return nodes that are deeper than depth (node.depth > depth)
117
118 The depth scopes are also available through calls to descendants, descendant_ids, subtree, subtree_ids, path and ancestors. In this case, depth values are interpreted relatively. Some examples:
119
120 node.subtree(:to_depth => 2) Subtree of node, to a depth of node.depth + 2 (self, children and grandchildren)
121 node.subtree.to_depth(5) Subtree of node to an absolute depth of 5
122 node.descendants(:at_depth => 2) Descendant of node, at depth node.depth + 2 (grandchildren)
123 node.descendants.at_depth(10) Descendants of node at an absolute depth of 10
124 node.ancestors.to_depth(3) The oldest 4 ancestors of node (its root and 3 more)
125 node.path(:from_depth => -2) The node's grandparent, parent and the node itself
126
127 node.ancestors(:from_depth => -6, :to_depth => -4)
128 node.path.from_depth(3).to_depth(4)
129 node.descendants(:from_depth => 2, :to_depth => 4)
130 node.subtree.from_depth(10).to_depth(12)
131
132 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.
1282d2e @stefankroes Initial commit
authored
133
e3ea26a @stefankroes - Version 1.1.2 (2009-10-29)
authored
134 = STI support
135
136 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.
137
79671c4 @stefankroes Started on rdoc readme
authored
138 = Arrangement
1282d2e @stefankroes Initial commit
authored
139
140 Ancestry can arrange an entire subtree into nested hashes for easy navigation after retrieval from the database. TreeNode.arrange could for example return:
141
8d2cffb @stefankroes Fixed some problems in the README
authored
142 { #<TreeNode id: 100018, name: "Stinky", ancestry: nil>
143 => { #<TreeNode id: 100019, name: "Crunchy", ancestry: "100018">
144 => { #<TreeNode id: 100020, name: "Squeeky", ancestry: "100018/100019">
145 => {}
146 }
1282d2e @stefankroes Initial commit
authored
147 }
148 }
149
150 The arrange method also works on a scoped class, for example:
151
8d2cffb @stefankroes Fixed some problems in the README
authored
152 TreeNode.find_by_name('Crunchy').subtree.arrange
1282d2e @stefankroes Initial commit
authored
153
ecea76f @stefankroes Version 1.2.0
authored
154 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 only works for Ruby 1.9 and later since before that hashes weren't ordered. For example:
155
156 TreeNode.find_by_name('Crunchy').subtree.arrange(:order => :name)
157
df705c3 @stefankroes Version 1.1.0 done!
authored
158 = Migrating from plugin that uses parent_id column
159
ecea76f @stefankroes Version 1.2.0
authored
160 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:
df705c3 @stefankroes Version 1.1.0 done!
authored
161
162 1. Add ancestry column to your table
90c4a10 @stefankroes Updated README
authored
163 - Create migration: <b>./script/generate migration add_ancestry_to_[table] ancestry:string</b>
164 - Add index to migration: <b>add_index [table], :ancestry</b> (UP) / <b>remove_index [table], :ancestry</b> (DOWN)
165 - Migrate your database: <b>rake db:migrate</b>
df705c3 @stefankroes Version 1.1.0 done!
authored
166
167 2. Remove old tree plugin or gem and add in Ancestry
168 - Remove plugin: rm -Rf vendor/plugins/[old plugin]
169 - Remove gem config line from environment.rb: config.gem [old gem]
170 - Add Ancestry to environment.rb: config.gem :ancestry
171 - See 'Installation' for more info on installing and configuring gems
172
173 3. Change your model
174 - Remove any macros required by old plugin/gem from app/models/[model].rb
ecea76f @stefankroes Version 1.2.0
authored
175 - Add to app/models/[model].rb: <b>has_ancestry</b>
df705c3 @stefankroes Version 1.1.0 done!
authored
176
90c4a10 @stefankroes Updated README
authored
177 4. Generate ancestry columns
178 - In './script.console': <b>[model].build_ancestry_from_parent_ids!</b>
179 - Make sure it worked ok: <b>[model].check_ancestry_integrity!</b>
df705c3 @stefankroes Version 1.1.0 done!
authored
180
181 5. Change your code
182 - Most tree calls will probably work fine with ancestry
183 - Others must be changed or proxied
184 - Check if all your data is intact and all tests pass
185
186 6. Drop parent_id column:
90c4a10 @stefankroes Updated README
authored
187 - Create migration: <b>./script/generate migration remove_parent_id_from_[table]</b>
188 - Add to migration: <b>remove_column [table], :parent_id</b> (UP) / <b>add_column [table], :parent_id, :integer</b> (DOWN)
189 - Migrate your database: <b>rake db:migrate</b>
df705c3 @stefankroes Version 1.1.0 done!
authored
190
41c344d @stefankroes Updated readme
authored
191 = Integrity checking and restoration
1282d2e @stefankroes Initial commit
authored
192
df705c3 @stefankroes Version 1.1.0 done!
authored
193 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.
194
195 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. To restore integrity use: [Model].restore_ancestry_integrity!.
1282d2e @stefankroes Initial commit
authored
196
197 For example, from IRB:
198
8d2cffb @stefankroes Fixed some problems in the README
authored
199 >> stinky = TreeNode.create :name => 'Stinky'
d43ece5 @stefankroes Some last changes to the readme
authored
200 $ #<TreeNode id: 1, name: "Stinky", ancestry: nil>
8d2cffb @stefankroes Fixed some problems in the README
authored
201 >> squeeky = TreeNode.create :name => 'Squeeky', :parent => stinky
d43ece5 @stefankroes Some last changes to the readme
authored
202 $ #<TreeNode id: 2, name: "Squeeky", ancestry: "1">
8d2cffb @stefankroes Fixed some problems in the README
authored
203 >> stinky.update_attribute :parent, squeeky
d43ece5 @stefankroes Some last changes to the readme
authored
204 $ true
8d2cffb @stefankroes Fixed some problems in the README
authored
205 >> TreeNode.all
d43ece5 @stefankroes Some last changes to the readme
authored
206 $ [#<TreeNode id: 1, name: "Stinky", ancestry: "1/2">, #<TreeNode id: 2, name: "Squeeky", ancestry: "1/2/1">]
df705c3 @stefankroes Version 1.1.0 done!
authored
207 >> TreeNode.check_ancestry_integrity!
8d2cffb @stefankroes Fixed some problems in the README
authored
208 !! Ancestry::AncestryIntegrityException: Conflicting parent id in node 1: 2 for node 1, expecting nil
df705c3 @stefankroes Version 1.1.0 done!
authored
209 >> TreeNode.restore_ancestry_integrity!
d43ece5 @stefankroes Some last changes to the readme
authored
210 $ [#<TreeNode id: 1, name: "Stinky", ancestry: 2>, #<TreeNode id: 2, name: "Squeeky", ancestry: nil>]
1282d2e @stefankroes Initial commit
authored
211
df705c3 @stefankroes Version 1.1.0 done!
authored
212 Additionally, if you think something is wrong with your depth cache:
213
214 >> TreeNode.rebuild_depth_cache!
1282d2e @stefankroes Initial commit
authored
215
df705c3 @stefankroes Version 1.1.0 done!
authored
216 = Tests
217
ecea76f @stefankroes Version 1.2.0
authored
218 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, install Ancestry as a plugin into a Rails application, go to the ancestry folder and type 'rake'. The test suite is located in 'test/has_ancestry_test.rb'.
1282d2e @stefankroes Initial commit
authored
219
79671c4 @stefankroes Started on rdoc readme
authored
220 = Internals
1282d2e @stefankroes Initial commit
authored
221
222 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.
223
df705c3 @stefankroes Version 1.1.0 done!
authored
224 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.
225
226 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.
227
228 = Version history
229
ecea76f @stefankroes Version 1.2.0
authored
230 The latest and recommended version of ancestry is 1.2.0. 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.
231
232 - Version 1.2.0 (2009-11-07)
233 - Removed some duplication in has_ancestry
234 - Cleaned up plugin pattern according to http://yehudakatz.com/2009/11/12/better-ruby-idioms/
235 - Moved parts of ancestry into seperate files
236 - Made it possible to pass options into the arrange method
237 - Renamed acts_as_tree to has_ancestry
238 - Aliased has_ancestry as acts_as_tree if acts_as_tree is available
239 - Added subtree_of scope
240 - Updated ordered_by_ancestry scope to support Microsoft SQL Server
241 - Added empty hash as parameter to exists? calls for older ActiveRecord versions
853e453 @stefankroes - Version 1.1.4 (2009-11-07)
authored
242 - Version 1.1.4 (2009-11-07)
243 - Thanks to a patch from tom taylor, Ancestry now works with different primary keys
b9a102a @stefankroes - Version 1.1.3 (2009-11-01)
authored
244 - Version 1.1.3 (2009-11-01)
245 - Fixed a pretty bad bug where several operations took far too many queries
e3ea26a @stefankroes - Version 1.1.2 (2009-10-29)
authored
246 - Version 1.1.2 (2009-10-29)
247 - Added validation for depth cache column
248 - Added STI support (reported broken)
48441fb @stefankroes - Version 1.1.1 (2009-10-28)
authored
249 - Version 1.1.1 (2009-10-28)
250 - Fixed some parentheses warnings that where reported
251 - Fixed a reported issue with arrangement
252 - Fixed issues with ancestors and path order on postgres
253 - Added ordered_by_ancestry scope (needed to fix issues)
df705c3 @stefankroes Version 1.1.0 done!
authored
254 - Version 1.1.0 (2009-10-22)
255 - Depth caching (and cache rebuilding)
256 - Depth method for nodes
257 - Named scopes for selecting by depth
258 - Relative depth options for tree navigation methods:
259 - ancestors
260 - path
261 - descendants
262 - descendant_ids
263 - subtree
264 - subtree_ids
265 - Updated README
266 - Easy migration from existing plugins/gems
267 - acts_as_tree checks unknown options
268 - acts_as_tree checks that options are hash
269 - Added a bang (!) to the integrity functions
ecea76f @stefankroes Version 1.2.0
authored
270 - 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.
df705c3 @stefankroes Version 1.1.0 done!
authored
271 - Updated install script to point to documentation
272 - Removed rails specific init
273 - Removed uninstall script
274 - Version 1.0.0 (2009-10-16)
275 - Initial version
276 - Tree building
277 - Tree navigation
278 - Integrity checking / restoration
279 - Arrangement
280 - Orphan strategies
281 - Subtree movement
282 - Named scopes
283 - Validations
1282d2e @stefankroes Initial commit
authored
284
41c344d @stefankroes Updated readme
authored
285 = Future work
1282d2e @stefankroes Initial commit
authored
286
df705c3 @stefankroes Version 1.1.0 done!
authored
287 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.
1282d2e @stefankroes Initial commit
authored
288
41c344d @stefankroes Updated readme
authored
289 = Contact and copyright
1282d2e @stefankroes Initial commit
authored
290
41c344d @stefankroes Updated readme
authored
291 Bug report? Faulty/incomplete documentation? Feature request? Please post an issue on 'http://github.com/stefankroes/ancestry/issues'. Please also contact me at s.a.kroes[at]gmail.com if it's urgent.
292
6e03a82 @stefankroes Added IRC channel to readme.
authored
293 Question? Contact me at s.a.kroes[at]gmail.com, make sure you read the documentation. You can also join the #ancestry channel on IRC (irc.freenode.net).
1282d2e @stefankroes Initial commit
authored
294
295 Copyright (c) 2009 Stefan Kroes, released under the MIT license
Something went wrong with that request. Please try again.