From c3d2274221e8b85fe3273aee86c7c0978be30153 Mon Sep 17 00:00:00 2001 From: Ken-ichi Ueda Date: Thu, 25 Nov 2010 20:09:43 -0800 Subject: [PATCH 1/3] Performance optimizations for rebuilding large trees. --- lib/ancestry/class_methods.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/ancestry/class_methods.rb b/lib/ancestry/class_methods.rb index 0887d341..ec287df4 100644 --- a/lib/ancestry/class_methods.rb +++ b/lib/ancestry/class_methods.rb @@ -52,7 +52,7 @@ def check_ancestry_integrity! options = {} parents = {} exceptions = [] if options[:report] == :list # For each node ... - self.base_class.all.each do |node| + self.base_class.find_each do |node| begin # ... check validity of ancestry column if !node.valid? and !node.errors[node.class.ancestry_column].blank? @@ -86,7 +86,7 @@ def check_ancestry_integrity! options = {} def restore_ancestry_integrity! parents = {} # For each node ... - self.base_class.all.each do |node| + self.base_class.find_each do |node| # ... set its ancestry to nil if invalid if node.errors[node.class.ancestry_column].blank? node.without_ancestry_callbacks do @@ -104,7 +104,7 @@ def restore_ancestry_integrity! parents[node.id] = nil if parent == node.id end # For each node ... - self.base_class.all.each do |node| + self.base_class.find_each do |node| # ... rebuild ancestry from parents array ancestry, parent = nil, parents[node.id] until parent.nil? @@ -118,7 +118,7 @@ def restore_ancestry_integrity! # Build ancestry from parent id's for migration purposes def build_ancestry_from_parent_ids! parent_id = nil, ancestry = nil - self.base_class.all(:conditions => {:parent_id => parent_id}).each do |node| + self.base_class.find_each(:conditions => {:parent_id => parent_id}) do |node| node.without_ancestry_callbacks do node.update_attribute ancestry_column, ancestry end @@ -129,7 +129,7 @@ def build_ancestry_from_parent_ids! parent_id = nil, ancestry = nil # Rebuild depth cache if it got corrupted or if depth caching was just turned on def rebuild_depth_cache! raise Ancestry::AncestryException.new("Cannot rebuild depth cache for model without depth caching.") unless respond_to? :depth_cache_column - self.base_class.all.each do |node| + self.base_class.find_each do |node| node.update_attribute depth_cache_column, node.depth end end From 5614a3b74da69ccdd3dca28869c2575146b07fb3 Mon Sep 17 00:00:00 2001 From: Ken-ichi Ueda Date: Thu, 25 Nov 2010 20:11:57 -0800 Subject: [PATCH 2/3] Added class method to sort nodes by ancestry. --- lib/ancestry/class_methods.rb | 30 ++++++++++++++++++++++++++---- test/has_ancestry_test.rb | 13 +++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/lib/ancestry/class_methods.rb b/lib/ancestry/class_methods.rb index ec287df4..c7184ef1 100644 --- a/lib/ancestry/class_methods.rb +++ b/lib/ancestry/class_methods.rb @@ -36,14 +36,36 @@ def arrange options = {} self.base_class.ordered_by_ancestry_and options.delete(:order) end # Get all nodes ordered by ancestry and start sorting them into an empty hash - scope.all(options).inject(ActiveSupport::OrderedHash.new) do |arranged_nodes, node| + arrange_nodes scope.all(options) + end + + # Arrange array of nodes into a nested hash of the form + # {node => children}, where children = {} if the node has no children + def arrange_nodes(nodes) + # Get all nodes ordered by ancestry and start sorting them into an empty hash + nodes.inject(ActiveSupport::OrderedHash.new) do |arranged_nodes, node| # Find the insertion point for that node by going through its ancestors node.ancestor_ids.inject(arranged_nodes) do |insertion_point, ancestor_id| insertion_point.each do |parent, children| # Change the insertion point to children if node is a descendant of this parent insertion_point = children if ancestor_id == parent.id - end; insertion_point - end[node] = ActiveSupport::OrderedHash.new; arranged_nodes + end + insertion_point + end[node] = ActiveSupport::OrderedHash.new + arranged_nodes + end + end + + # Pseudo-preordered array of nodes. Children will always follow parents, + # but the ordering of nodes within a rank depends on their order in the + # array that gets passed in + def sort_by_ancestry(nodes) + arranged = nodes.is_a?(Hash) ? nodes : arrange_nodes(nodes.sort_by{|n| n.ancestry || '0'}) + arranged.inject([]) do |sorted_nodes, pair| + node, children = pair + sorted_nodes << node + sorted_nodes += sort_by_ancestry(children) unless children.blank? + sorted_nodes end end @@ -134,4 +156,4 @@ def rebuild_depth_cache! end end end -end \ No newline at end of file +end diff --git a/test/has_ancestry_test.rb b/test/has_ancestry_test.rb index 7fe6687e..dca1d90c 100644 --- a/test/has_ancestry_test.rb +++ b/test/has_ancestry_test.rb @@ -688,4 +688,17 @@ def test_arrange_order_option end end end + + def test_sort_by_ancestry + AncestryTestDatabase.with_model do |model| + n1 = model.create! + n2 = model.create!(:parent => n1) + n3 = model.create!(:parent => n2) + n4 = model.create!(:parent => n2) + n5 = model.create!(:parent => n1) + + arranged = model.sort_by_ancestry(model.all.sort_by(&:id).reverse) + assert_equal [n1, n2, n4, n3, n5].map(&:id), arranged.map(&:id) + end + end end From adb9acd1afa0d3322378c9326782c40cfe789f05 Mon Sep 17 00:00:00 2001 From: Ken-ichi Ueda Date: Mon, 20 Dec 2010 18:20:29 -0500 Subject: [PATCH 3/3] Documented the sort_by_ancestry method in the README. --- README.rdoc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.rdoc b/README.rdoc index cba5dc17..907e1f90 100644 --- a/README.rdoc +++ b/README.rdoc @@ -156,6 +156,14 @@ The arrange method takes ActiveRecord find options. If you want your hashes to b 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: