2007-01-22 14:43:50 +01:00
module ActiveRecord
module Acts #:nodoc:
module NestedSet #:nodoc:
2007-02-09 09:04:31 +01:00
def self . included ( base )
2007-01-22 14:43:50 +01:00
base . extend ( ClassMethods )
end
# This acts provides Nested Set functionality. Nested Set is similiar to Tree, but with
# the added feature that you can select the children and all of their descendents with
# a single query. A good use case for this is a threaded post system, where you want
# to display every reply to a comment without multiple selects.
#
# A google search for "Nested Set" should point you in the direction to explain the
# database theory. I figured out a bunch of this from
# http://threebit.net/tutorials/nestedset/tutorial1.html
#
# Instead of picturing a leaf node structure with children pointing back to their parent,
# the best way to imagine how this works is to think of the parent entity surrounding all
# of its children, and its parent surrounding it, etc. Assuming that they are lined up
# horizontally, we store the left and right boundries in the database.
#
# Imagine:
# root
# |_ Child 1
# |_ Child 1.1
# |_ Child 1.2
# |_ Child 2
# |_ Child 2.1
# |_ Child 2.2
#
# If my cirlces in circles description didn't make sense, check out this sweet
# ASCII art:
#
# ___________________________________________________________________
# | Root |
# | ____________________________ ____________________________ |
# | | Child 1 | | Child 2 | |
# | | __________ _________ | | __________ _________ | |
# | | | C 1.1 | | C 1.2 | | | | C 2.1 | | C 2.2 | | |
# 1 2 3_________4 5________6 7 8 9_________10 11_______12 13 14
# | |___________________________| |___________________________| |
# |___________________________________________________________________|
#
# The numbers represent the left and right boundries. The table then might
# look like this:
# ID | PARENT | LEFT | RIGHT | DATA
# 1 | 0 | 1 | 14 | root
# 2 | 1 | 2 | 7 | Child 1
# 3 | 2 | 3 | 4 | Child 1.1
# 4 | 2 | 5 | 6 | Child 1.2
# 5 | 1 | 8 | 13 | Child 2
# 6 | 5 | 9 | 10 | Child 2.1
# 7 | 5 | 11 | 12 | Child 2.2
#
# So, to get all children of an entry, you
# SELECT * WHERE CHILD.LEFT IS BETWEEN PARENT.LEFT AND PARENT.RIGHT
#
# To get the count, it's (LEFT - RIGHT + 1)/2, etc.
#
# To get the direct parent, it falls back to using the PARENT_ID field.
#
# There are instance methods for all of these.
#
# The structure is good if you need to group things together; the downside is that
# keeping data integrity is a pain, and both adding and removing an entry
# require a full table write.
#
# This sets up a before_destroy trigger to prune the tree correctly if one of its
# elements gets deleted.
#
module ClassMethods
# Configuration options are:
#
# * +parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
# * +left_column+ - column name for left boundry data, default "lft"
# * +right_column+ - column name for right boundry data, default "rgt"
# * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
# (if that hasn't been already) and use that as the foreign key restriction. It's also possible
# to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
# Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
def acts_as_nested_set ( options = { } )
configuration = { :parent_column = > " parent_id " , :left_column = > " lft " , :right_column = > " rgt " , :scope = > " 1 = 1 " }
configuration . update ( options ) if options . is_a? ( Hash )
configuration [ :scope ] = " #{ configuration [ :scope ] } _id " . intern if configuration [ :scope ] . is_a? ( Symbol ) && configuration [ :scope ] . to_s !~ / _id$ /
if configuration [ :scope ] . is_a? ( Symbol )
scope_condition_method = %(
def scope_condition
if #{configuration[:scope].to_s}.nil?
" #{ configuration [ :scope ] . to_s } IS NULL "
else
" #{ configuration [ :scope ] . to_s } = \# { #{ configuration [ :scope ] . to_s } } "
end
end
)
else
scope_condition_method = " def scope_condition() \" #{ configuration [ :scope ] } \" end "
end
class_eval <<-EOV
include ActiveRecord :: Acts :: NestedSet :: InstanceMethods
#{scope_condition_method}
def left_col_name ( ) " #{ configuration [ :left_column ] } " end
def right_col_name ( ) " #{ configuration [ :right_column ] } " end
def parent_column ( ) " #{ configuration [ :parent_column ] } " end
EOV
end
end
module InstanceMethods
# Returns true is this is a root node.
def root?
parent_id = self [ parent_column ]
( parent_id == 0 || parent_id . nil? ) && ( self [ left_col_name ] == 1 ) && ( self [ right_col_name ] > self [ left_col_name ] )
end
# Returns true is this is a child node
def child?
parent_id = self [ parent_column ]
! ( parent_id == 0 || parent_id . nil? ) && ( self [ left_col_name ] > 1 ) && ( self [ right_col_name ] > self [ left_col_name ] )
end
# Returns true if we have no idea what this is
def unknown?
! root? && ! child?
end
# Adds a child to this object in the tree. If this object hasn't been initialized,
# it gets set up as a root node. Otherwise, this method will update all of the
# other elements in the tree and shift them to the right, keeping everything
# balanced.
def add_child ( child )
self . reload
child . reload
if child . root?
raise " Adding sub-tree isn \' t currently supported "
else
if ( ( self [ left_col_name ] == nil ) || ( self [ right_col_name ] == nil ) )
# Looks like we're now the root node! Woo
self [ left_col_name ] = 1
self [ right_col_name ] = 4
# What do to do about validation?
return nil unless self . save
child [ parent_column ] = self . id
child [ left_col_name ] = 2
child [ right_col_name ] = 3
return child . save
else
# OK, we need to add and shift everything else to the right
child [ parent_column ] = self . id
right_bound = self [ right_col_name ]
child [ left_col_name ] = right_bound
child [ right_col_name ] = right_bound + 1
self [ right_col_name ] += 2
2007-02-09 09:04:31 +01:00
self . class . base_class . transaction {
self . class . base_class . update_all ( " #{ left_col_name } = ( #{ left_col_name } + 2) " , " #{ scope_condition } AND #{ left_col_name } >= #{ right_bound } " )
self . class . base_class . update_all ( " #{ right_col_name } = ( #{ right_col_name } + 2) " , " #{ scope_condition } AND #{ right_col_name } >= #{ right_bound } " )
2007-01-22 14:43:50 +01:00
self . save
child . save
}
end
end
end
# Returns the number of nested children of this object.
def children_count
return ( self [ right_col_name ] - self [ left_col_name ] - 1 ) / 2
end
# Returns a set of itself and all of its nested children
def full_set
2007-02-09 09:04:31 +01:00
self . class . base_class . find ( :all , :conditions = > " #{ scope_condition } AND ( #{ left_col_name } BETWEEN #{ self [ left_col_name ] } and #{ self [ right_col_name ] } ) " )
2007-01-22 14:43:50 +01:00
end
# Returns a set of all of its children and nested children
def all_children
2007-02-09 09:04:31 +01:00
self . class . base_class . find ( :all , :conditions = > " #{ scope_condition } AND ( #{ left_col_name } > #{ self [ left_col_name ] } ) and ( #{ right_col_name } < #{ self [ right_col_name ] } ) " )
2007-01-22 14:43:50 +01:00
end
# Returns a set of only this entry's immediate children
def direct_children
2007-02-09 09:04:31 +01:00
self . class . base_class . find ( :all , :conditions = > " #{ scope_condition } and #{ parent_column } = #{ self . id } " )
2007-01-22 14:43:50 +01:00
end
# Prunes a branch off of the tree, shifting all of the elements on the right
# back to the left so the counts still work.
def before_destroy
return if self [ right_col_name ] . nil? || self [ left_col_name ] . nil?
dif = self [ right_col_name ] - self [ left_col_name ] + 1
2007-02-09 09:04:31 +01:00
self . class . base_class . transaction {
self . class . base_class . delete_all ( " #{ scope_condition } and #{ left_col_name } > #{ self [ left_col_name ] } and #{ right_col_name } < #{ self [ right_col_name ] } " )
self . class . base_class . update_all ( " #{ left_col_name } = ( #{ left_col_name } - #{ dif } ) " , " #{ scope_condition } AND #{ left_col_name } >= #{ self [ right_col_name ] } " )
self . class . base_class . update_all ( " #{ right_col_name } = ( #{ right_col_name } - #{ dif } ) " , " #{ scope_condition } AND #{ right_col_name } >= #{ self [ right_col_name ] } " )
2007-01-22 14:43:50 +01:00
}
end
end
end
end
end