在Rails中使用has_one 、has_many 、belongs_to 和 has_and_belongs_to_may 來聲明關係型數據庫中的一對一,一對多和多對多的關係,但當想以樹形的數據結構來表示分類的時候,這些基本的關聯功能並不夠,Rails在has_XXX關係的基礎上,提供了acts as的擴展功能,如acts_as_list 、acts_as_tree 、 acts_as_nested_set。acts_as_tree就提供樹狀的結構來組織記錄。(不知道爲什麼Rails2.0以後會取消掉,需要通過插件的方式來安裝)
acts_as_nested_set的官方解釋:
上面是引用自rubyonrails.org上的對於acts_as_nested_set的描述,並提供了一個簡單的示例:
SQL腳本:
create table nested_objects (
id int(11) unsigned not null auto_increment,
parent_id int(11),
lft int(11),
rgt int(11),
name varchar(32),
primary key (id)
);
Ruby Model:
class NestedObject < ActiveRecord::Base
acts_as_nested_set
end
acts_as_nested_set提供的方法:
- root?() – 是否是根對象
- child?() – 是否是子對象(擁有父對象)
- unknown?() – 不知道該對象的狀態(既不是根對象,也不是子對象)
- add_child(child_object) – 爲根對象添加一個子對象(如果child_object是一個根對象的話,則添加失敗)
- children_count() – 根對象的所有子對象的個數
- full_set() – 重新找到所有對象
- all_children() – 根對象的所有子對象
- direct_children() –根對象的直接子對象
下面就使用acts_as_nested_set來生成一個Ext的Tree。
比如生成如下的樹:
root |_ Child 1 | |_ Child 1.1 | |_ Child 1.2 |_ Child 2 |_ Child 2.1 |_ Child 2.2
先來看一下對上面的樹的一個圖形化的解釋:
這圖還是比較清除的,請理解橫線中的1到14這些數字,對應這個樹,我們可能會有下面的數據:
這個也就是SQL腳本中的的lft和rgt的解釋。
1.創建Rails工程:
rails ExtTree
2.安裝act_as_nested_set:
ruby script/plugin install acts_as_nested_set
3.下載ext,解壓下載後的壓縮包並拷貝到ExtTree工程的public目錄(public/ext)
4.創建模型對象:
ruby script/generate resource Category parent_id:integer lft:integer rgt:integer text:string
5.給模型對象Category加入acts_as_nested_set:
class Category < ActiveRecord::Base
acts_as_nested_set
end
6.下面在CategoriesController中加入index方法,讓它來轉到index.html頁面,並且爲EXT TREE來生成JSON數據:
class CategoriesController < ApplicationController
def index(id = params[:node])
respond_to do |format|
format.html # render static index.html.erb
format.json { render :json => Category.find_children(id) }
end
end
end
index方法有一個參數id,用來接收一個樹的節點的id,我們就可以通過一個id來查找該節點的子節點。
7.實現CategoriesController中的find_children方法:
#首先先得到樹的根節點,再根據傳過來的id找到根的子節點
def self.find_children(start_id = nil)
start_id.to_i == 0 ? root_nodes : find(start_id).direct_children
end
#如果parent_id爲空,則爲樹的根節點
def self.root_nodes
find(:all, :conditions => 'parent_id IS NULL')
end
到這裏,已經實現了基本的樹形結構,但卻還有一個問題,如果是樹葉節點,既沒有子節點的節點,圖標應該顯示爲"-" ,不應該再能夠伸展了,Ext Tree中提供的示例中給出的JSON數據中有一個leaf的屬性,如果爲true,則爲樹葉節點,如果爲false,則爲樹枝節點,所以,我們還需要讓我們生成的JSON數據用來leaf來標識樹枝節點與樹葉節點,在Category.rb中添加如下代碼:
def leaf
unknown? || children_count == 0
end
def to_json_with_leaf(options = {})
self.to_json_without_leaf(options.merge(:methods => :leaf))
end
alias_method_chain :to_json, :leaf
對於alias_method_chain,需要先說一下Ruby中的alias_method方法,在Ruby中有這樣的用法:
alias_method :old_method_name :new_method_name
它同alias很類似,但只能用法方法。
在Ruby中,可以使用方法鏈的手段來實現mix-in,如果想要用new_method來override old_method方法,就可以這樣使用:
alias_method :old_method_name :new_method_name
alias_method :new_method_name :old_method_name
而在Rails中,提供了一個更強大的方法:alias_method_chain。
下面是index.html.erb文件:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
<title>Rails Ext Tree</title>
<%= stylesheet_link_tag "../ext/resources/css/ext-all.css" %>
<%= javascript_include_tag :defaults %>
<%= javascript_include_tag "../ext/adapter/prototype/ext-prototype-adapter.js" %>
<%= javascript_include_tag "../ext/ext-all.js" %>
</head>
<body>
<div id="category-tree" style="padding:20px"></div>
<% javascript_tag do -%>
Ext.onReady(function(){
root = new Ext.tree.AsyncTreeNode({
text: 'Invisible Root',
id:'0'
});
new Ext.tree.TreePanel({
loader: new Ext.tree.TreeLoader({
url:'/categories',
requestMethod:'GET',
baseParams:{format:'json'}
}),
renderTo:'category-tree',
root: root,
rootVisible:false
});
root.expand();
});
<% end -%>
</body>
</html>
添加測試數據:
root = Category.create(:text => 'Root')
root.add_child(c1 = Category.create(:text => 'Child 1'))
c1.add_child(Category.create(:text => 'Child 1.1'))
c1.add_child(Category.create(:text => 'Child 1.2'))
root.add_child(c2 = Category.create(:text => 'Child 2'))
c2.add_child(c21 = Category.create(:text => 'Child 2.1'))
c2.add_child(c21 = Category.create(:text => 'Child 2.2'))
最後的顯示效果: