mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Added acts_as_nested_set #1000 [wschenk]
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@1185 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
parent
8e8bf37aa9
commit
339f4956b3
6 changed files with 270 additions and 0 deletions
|
@ -1,5 +1,12 @@
|
|||
*SVN*
|
||||
|
||||
* Added acts_as_nested_set #1000 [wschenk]. Introduction:
|
||||
|
||||
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 it's descendants 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.
|
||||
|
||||
* Added insert_at(position) to acts_as_list #1083 [DeLynnB]
|
||||
|
||||
* Removed the default order by id on has_and_belongs_to_many queries as it could kill performance on large sets (you can still specify by hand with :order)
|
||||
|
|
|
@ -43,6 +43,7 @@ require 'active_record/reflection'
|
|||
require 'active_record/timestamp'
|
||||
require 'active_record/acts/list'
|
||||
require 'active_record/acts/tree'
|
||||
require 'active_record/acts/nested_set'
|
||||
require 'active_record/locking'
|
||||
require 'active_record/migration'
|
||||
|
||||
|
@ -57,6 +58,7 @@ ActiveRecord::Base.class_eval do
|
|||
include ActiveRecord::Reflection
|
||||
include ActiveRecord::Acts::Tree
|
||||
include ActiveRecord::Acts::List
|
||||
include ActiveRecord::Acts::NestedSet
|
||||
end
|
||||
|
||||
require 'active_record/connection_adapters/mysql_adapter'
|
||||
|
|
212
activerecord/lib/active_record/acts/nested_set.rb
Normal file
212
activerecord/lib/active_record/acts/nested_set.rb
Normal file
|
@ -0,0 +1,212 @@
|
|||
module ActiveRecord
|
||||
module Acts #:nodoc:
|
||||
module NestedSet #:nodoc:
|
||||
def self.append_features(base)
|
||||
super
|
||||
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 it's descendants 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
|
||||
# data base theory. I figured a bunch of this from
|
||||
# http://threebit.net/tutorials/nestedset/tutorial1.html
|
||||
#
|
||||
# Instead of picturing a leaf node structure with child pointing back to their parent,
|
||||
# the best way to imagine how this works is to think of the parent entity surrounding all
|
||||
# of it's children, and it's 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 them 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 and entry
|
||||
# require a full table write.
|
||||
#
|
||||
# This sets up a before_destroy trigger to prune the tree correctly if one of it's
|
||||
# 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
|
||||
|
||||
|
||||
# Added 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
|
||||
# balanaced.
|
||||
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
|
||||
self.class.transaction {
|
||||
self.class.update_all( "#{left_col_name} = (#{left_col_name} + 2)", "#{scope_condition} AND #{left_col_name} >= #{right_bound}" )
|
||||
self.class.update_all( "#{right_col_name} = (#{right_col_name} + 2)", "#{scope_condition} AND #{right_col_name} >= #{right_bound}" )
|
||||
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 it's nested children
|
||||
def full_set
|
||||
self.class.find_all( "#{scope_condition} AND (#{left_col_name} BETWEEN #{self[left_col_name]} and #{self[right_col_name]})" )
|
||||
end
|
||||
|
||||
# Returns a set of all of it's children and nested children
|
||||
def all_children
|
||||
self.class.find_all( "#{scope_condition} AND (#{left_col_name} > #{self[left_col_name]}) and (#{right_col_name} < #{self[right_col_name]})" )
|
||||
end
|
||||
|
||||
# Returns a set of only this entries immediate children
|
||||
def direct_children
|
||||
self.class.find_all( "#{scope_condition} and #{parent_column} = #{self.id}")
|
||||
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
|
||||
|
||||
self.class.transaction {
|
||||
self.class.delete_all( "#{scope_condition} and #{left_col_name} > #{self[left_col_name]} and #{right_col_name} < #{self[right_col_name]}" )
|
||||
self.class.update_all( "#{left_col_name} = (#{left_col_name} - #{dif})", "#{scope_condition} AND #{left_col_name} >= #{self[right_col_name]}" )
|
||||
self.class.update_all( "#{right_col_name} = (#{right_col_name} - #{dif} )", "#{scope_condition} AND #{right_col_name} >= #{self[right_col_name]}" )
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
18
activerecord/test/fixtures/mixin.rb
vendored
18
activerecord/test/fixtures/mixin.rb
vendored
|
@ -16,5 +16,23 @@ end
|
|||
class ListWithStringScopeMixin < ActiveRecord::Base
|
||||
acts_as_list :column => "pos", :scope => 'parent_id = #{parent_id}'
|
||||
|
||||
def self.table_name() "mixins" end
|
||||
end
|
||||
|
||||
class NestedSet < Mixin
|
||||
acts_as_nested_set :scope => "ROOT_ID IS NULL"
|
||||
|
||||
def self.table_name() "mixins" end
|
||||
end
|
||||
|
||||
class NestedSetWithStringScope < Mixin
|
||||
acts_as_nested_set :scope => 'root_id = #{root_id}'
|
||||
|
||||
def self.table_name() "mixins" end
|
||||
end
|
||||
|
||||
class NestedSetWithSymbolScope < Mixin
|
||||
acts_as_nested_set :scope => :root
|
||||
|
||||
def self.table_name() "mixins" end
|
||||
end
|
30
activerecord/test/fixtures/mixins.yml
vendored
30
activerecord/test/fixtures/mixins.yml
vendored
|
@ -28,3 +28,33 @@ list_<%= counter %>:
|
|||
type: ListMixin
|
||||
parent_id: 5
|
||||
<% end %>
|
||||
|
||||
# Nested set mixins
|
||||
|
||||
<% (1..10).each do |counter| %>
|
||||
set_<%= counter %>:
|
||||
id: <%= counter+3000 %>
|
||||
type: NestedSet
|
||||
<% end %>
|
||||
|
||||
# Big old set
|
||||
<%
|
||||
[[4001, 0, 1, 20],
|
||||
[4002, 4001, 2, 7],
|
||||
[4003, 4002, 3, 4],
|
||||
[4004, 4002, 5, 6],
|
||||
[4005, 4001, 8, 13],
|
||||
[4006, 4005, 9, 10],
|
||||
[4007, 4005, 11, 12],
|
||||
[4008, 4001, 14, 19],
|
||||
[4009, 4008, 15, 16],
|
||||
[4010, 4008, 17, 18]].each do |set| %>
|
||||
tree_<%= set[0] %>:
|
||||
id: <%= set[0]%>
|
||||
parent_id: <%= set[1]%>
|
||||
type: NestedSetWithStringScope
|
||||
lft: <%= set[2]%>
|
||||
rgt: <%= set[3]%>
|
||||
root_id: 42
|
||||
|
||||
<% end %>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
require 'abstract_unit'
|
||||
require 'active_record/acts/tree'
|
||||
require 'active_record/acts/list'
|
||||
require 'active_record/acts/nested_set'
|
||||
require 'fixtures/mixin'
|
||||
|
||||
class ListTest < Test::Unit::TestCase
|
||||
|
|
Loading…
Reference in a new issue