Skip to content

Commit

Permalink
added level cache
Browse files Browse the repository at this point in the history
  • Loading branch information
Christophe Pollet committed Mar 5, 2010
1 parent bac1762 commit 7922b86
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 45 deletions.
62 changes: 45 additions & 17 deletions lib/active_record/acts/tree.rb
Expand Up @@ -41,30 +41,48 @@ module ClassMethods
# * <tt>order</tt> - makes it possible to sort the children according to this SQL snippet.
# * <tt>counter_cache</tt> - keeps a count in a +children_count+ column if set to +true+ (default: +false+).
def acts_as_tree(options = {})
configuration = { :foreign_key => "parent_id", :order => nil, :counter_cache => nil, :dependent => :destroy, :touch => false }
configuration = {
:foreign_key => "parent_id",
:order => nil,
:counter_cache => nil,
:dependent => :destroy,
:touch => false
}
configuration.update(options) if options.is_a?(Hash)

belongs_to :parent, :class_name => name, :foreign_key => configuration[:foreign_key], :counter_cache => configuration[:counter_cache], :touch => configuration[:touch]
has_many :children, :class_name => name, :foreign_key => configuration[:foreign_key], :order => configuration[:order], :dependent => configuration[:dependent]


belongs_to :parent,
:class_name => name,
:foreign_key => configuration[:foreign_key],
:counter_cache => configuration[:counter_cache],
:touch => configuration[:touch]

has_many :children,
:class_name => name,
:foreign_key => configuration[:foreign_key],
:order => configuration[:order],
:dependent => configuration[:dependent]

class_eval <<-EOV
include ActiveRecord::Acts::Tree::InstanceMethods
named_scope :roots, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}}
named_scope :roots,
:conditions => "#{configuration[:foreign_key]} IS NULL",
:order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}}
after_save :update_level_cache
after_update :update_parents_counter_cache
def self.root
roots.first
end
def self.childless
nodes = []
find(:all).each do |node|
nodes << node if node.children.empty?
end
nodes
end
Expand All @@ -80,7 +98,7 @@ def self.childless
EOV
end
end

module InstanceMethods
# Returns list of ancestors, starting from parent until root.
#
Expand All @@ -89,27 +107,27 @@ def ancestors
node, nodes = self, []
nodes << node = node.parent until node.parent.nil? and return nodes
end

# Returns the root node of the tree.
def root
node = self
node = node.parent until node.parent.nil? and return node
end

# Returns all siblings of the current node.
#
# subchild1.siblings # => [subchild2]
def siblings
self_and_siblings - [self]
end

# Returns all siblings and a reference to the current node.
#
# subchild1.self_and_siblings # => [subchild1, subchild2]
def self_and_siblings
parent ? parent.children : self.class.roots
end

# Returns a flat list of the descendants of the current node.
#
# root.descendants # => [child1, subchild1, subchild2]
Expand All @@ -127,15 +145,25 @@ def descendants(node=self)
def childless
self.descendants.collect{|d| d.children.empty? ? d : nil}.compact
end

private

private
def update_parents_counter_cache
if self.respond_to?(:children_count) && parent_id_changed?
self.class.decrement_counter(:children_count, parent_id_was)
self.class.increment_counter(:children_count, parent_id)
end
end

def update_level_cache
if respond_to?(:level_cache) && parent_id_changed?
_level_cache = ancestors.length

if level_cache != _level_cache
self.class.update_all("level_cache = #{_level_cache}", ['id = ?', id])
end
end
end
end
end
end
Expand Down
91 changes: 63 additions & 28 deletions test/acts_as_tree_test.rb
Expand Up @@ -22,6 +22,7 @@ def assert_no_queries(&block)
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")

# AR keeps printing annoying schema statements
$stdout_orig = $stdout
$stdout = StringIO.new

def setup_db
Expand All @@ -31,6 +32,7 @@ def setup_db
t.column :type, :string
t.column :parent_id, :integer
t.column :children_count, :integer, :default => 0
t.column :level_cache, :integer, :default => 0
end
end
end
Expand All @@ -44,7 +46,7 @@ def teardown_db
class Mixin < ActiveRecord::Base
end

class TreeMixin < Mixin
class TreeMixin < Mixin
acts_as_tree :foreign_key => "parent_id", :order => "id"
end

Expand All @@ -66,7 +68,6 @@ class TreeMixinNullify < Mixin
end

class TreeTest < Test::Unit::TestCase

def setup
setup_db
@root1 = TreeMixin.create!
Expand All @@ -76,24 +77,24 @@ def setup
@root2 = TreeMixin.create!
@root3 = TreeMixin.create!
end

def teardown
teardown_db
end

def test_children
assert_equal @root1.reload.children, [@root_child1, @root_child2]
assert_equal @root_child1.reload.children, [@child1_child]
assert_equal @child1_child.reload.children, []
assert_equal @root_child2.reload.children, []
end

def test_parent
assert_equal @root_child1.parent, @root1
assert_equal @root_child1.parent, @root_child2.parent
assert_nil @root1.parent
end

def test_nullify
root4 = TreeMixinNullify.create!
root4_child = TreeMixinNullify.create! :parent_id => root4.id
Expand All @@ -103,7 +104,7 @@ def test_nullify
assert_equal 1, TreeMixinNullify.count
assert_nil root4_child.reload.parent_id
end

def test_delete
assert_equal 6, TreeMixin.count
@root1.destroy
Expand All @@ -112,20 +113,20 @@ def test_delete
@root3.destroy
assert_equal 0, TreeMixin.count
end

def test_insert
@extra = @root1.children.create

assert @extra

assert_equal @extra.parent, @root1

assert_equal 3, @root1.reload.children.count
assert @root1.children.include?(@extra)
assert @root1.children.include?(@root_child1)
assert @root1.children.include?(@root_child2)
end

def test_ancestors
assert_equal [], @root1.ancestors
assert_equal [@root1], @root_child1.ancestors
Expand All @@ -134,7 +135,7 @@ def test_ancestors
assert_equal [], @root2.ancestors
assert_equal [], @root3.ancestors
end

def test_root
assert_equal @root1, TreeMixin.root
assert_equal @root1, @root1.root
Expand All @@ -144,11 +145,11 @@ def test_root
assert_equal @root2, @root2.root
assert_equal @root3, @root3.root
end

def test_roots
assert_equal [@root1, @root2, @root3], TreeMixin.roots
end

def test_siblings
assert_equal [@root2, @root3], @root1.siblings
assert_equal [@root_child2], @root_child1.siblings
Expand All @@ -157,7 +158,7 @@ def test_siblings
assert_equal [@root1, @root3], @root2.siblings
assert_equal [@root1, @root2], @root3.siblings
end

def test_self_and_siblings
assert_equal [@root1, @root2, @root3], @root1.self_and_siblings
assert_equal [@root_child1, @root_child2], @root_child1.self_and_siblings
Expand Down Expand Up @@ -196,10 +197,45 @@ def test_update_parents_counter_cache

end

class TreeTestWithLevelCache < Test::Unit::TestCase
def setup
teardown_db
setup_db
@root1 = TreeMixin.create!
@root_child1 = TreeMixin.create! :parent_id => @root1.id
@child1_child = TreeMixin.create! :parent_id => @root_child1.id
@root_child2 = TreeMixin.create! :parent_id => @root1.id
@root2 = TreeMixin.create!
end

def teardown
teardown_db
end

def test_level_cache
assert_equal 0, @root1.reload.level_cache
assert_equal 1, @root_child1.reload.level_cache
assert_equal 2, @child1_child.reload.level_cache
assert_equal 0, @root2.reload.level_cache
end

def test_level_cache_are_updated
@child1_child.reload.parent_id = nil
@child1_child.save

@root2.reload.parent_id = @root_child2.reload.id
@root2.save

assert_equal 0, @root1.reload.level_cache
assert_equal 1, @root_child1.reload.level_cache
assert_equal 0, @child1_child.reload.level_cache
assert_equal 2, @root2.reload.level_cache
end
end

class TreeTestWithEagerLoading < Test::Unit::TestCase

def setup
def setup
teardown_db
setup_db
@root1 = TreeMixin.create!
Expand All @@ -210,23 +246,23 @@ def setup
@root3 = TreeMixin.create!

@rc1 = RecursivelyCascadedTreeMixin.create!
@rc2 = RecursivelyCascadedTreeMixin.create! :parent_id => @rc1.id
@rc2 = RecursivelyCascadedTreeMixin.create! :parent_id => @rc1.id
@rc3 = RecursivelyCascadedTreeMixin.create! :parent_id => @rc2.id
@rc4 = RecursivelyCascadedTreeMixin.create! :parent_id => @rc3.id
end

def teardown
teardown_db
end

def test_eager_association_loading
roots = TreeMixin.find(:all, :include => :children, :conditions => "mixins.parent_id IS NULL", :order => "mixins.id")
assert_equal [@root1, @root2, @root3], roots
assert_equal [@root1, @root2, @root3], roots
assert_no_queries do
assert_equal 2, roots[0].children.count
assert_equal 0, roots[1].children.count
assert_equal 0, roots[2].children.count
end
end
end

def test_eager_association_loading_with_recursive_cascading_three_levels_has_many
Expand All @@ -242,26 +278,25 @@ def test_eager_association_loading_with_recursive_cascading_three_levels_has_one
def test_eager_association_loading_with_recursive_cascading_three_levels_belongs_to
leaf_node = RecursivelyCascadedTreeMixin.find(:first, :include => { :parent => { :parent => :parent } }, :order => 'mixins.id DESC')
assert_equal @rc1, assert_no_queries { leaf_node.parent.parent.parent }
end
end
end

class TreeTestWithoutOrder < Test::Unit::TestCase

def setup
def setup
setup_db
@root1 = TreeMixinWithoutOrder.create!
@root2 = TreeMixinWithoutOrder.create!
end

def teardown
teardown_db
end

def test_root
assert [@root1, @root2].include?(TreeMixinWithoutOrder.root)
end

def test_roots
assert_equal [], [@root1, @root2] - TreeMixinWithoutOrder.roots
end
end
end

0 comments on commit 7922b86

Please sign in to comment.