在 Rails 中處理樹狀結構有一些套件可用,本篇文章將比較下面幾種解決方案:
- ancestry
- awesome_nested_set
- closure_tree
情境
假設有個 model:Node,可以樹狀加入子節點。
功能
ancestry
設定
使用新的規則,config/initializers/ancestry.rb 加入
1
| Ancestry.default_ancestry_format = :materialized_path2
|
需要使用樹狀結構的資料表要增加一些專用欄位
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| create_table :nodes do |t| t.string :name, null: false
t.string :ancestry, collation: 'C', null: false
t.integer :ancestry_depth, default: 0 t.integer :children_count, default: 0
t.timestamps
t.index :ancestry end
|
然後在 Model 加入 has_ancestry
1 2 3 4 5 6 7
| class Node < ApplicationRecord
has_ancestry counter_cache: true, cache_depth: true end
|
基本功能
列出常用的 instance methods
1 2 3 4 5 6
| node.children node.ancestors node.descendants
node.siblings
|
列出常用的 class methods
1 2 3 4 5
| Node.roots Node.children_of(id_or_node) Node.ancestor_of(id_or_node) Node.descendants_of(id_or_node) Node.siblings_of(id_or_node)
|
Ransack 搜索
利用 class methods 和 ransack 搭配使用
1 2 3 4 5 6 7
| def self.ransackable_attributes(_auth_object = nil) %w[ancestry] end
def self.ransackable_scopes(_auth_object = nil) %w[children_of] end
|
就可以
1
| Node.ransack(children_of: 10)
|
但要搜索 roots 則要查詢 ancestry
1
| Node.ransack(ancestry: '/')
|
awesome_nested_set
設定
需要使用樹狀結構的資料表要增加一些專用欄位
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| create_table :nodes do |t| t.string :name
t.integer :parent_id, null: true, index: true t.integer :lft, null: false, index: true t.integer :rgt, null: false, index: true
t.integer :depth, null: false, default: 0 t.integer :children_count, null: false, default: 0
t.timestamps end
|
然後在 Model 加入 acts_as_nested_set
1 2 3 4 5 6 7
| class Node < ActiveRecord::Base
acts_as_nested_set counter_cache: :children_count end
|
基本功能
列出常用的 instance methods
1 2 3 4
| node.children node.ancestors node.descendants node.siblings
|
如果要包含自己,採用比較明確的命名
1 2 3
| node.self_and_ancestors node.self_and_siblings node.self_and_descendants
|
class methods 主要只有 roots 可以用
Ransack 搜索
可以直接利用 parent_id 搜索
1 2 3
| def self.ransackable_attributes(_auth_object = nil) %w[parent_id] end
|
然後
1 2 3 4
| Node.ransack(parent_id: 10)
Node.ransack(parent_id: nil)
|
如果需要其他類型的搜索,可以自己定義 scope 來使用。
closure_tree
設定
和其他套件不同,設定方式相對複雜,要照著官方文件的步驟進行。最後會有一個額外的表來紀錄樹狀結構,以上面的 Node 為例,會生成另一個 node_hierarchies 的表。
基本功能
和 awesome_nested_set 差不多,這邊就不重複了。不過和前兩個套件比,資料庫沒有存 children_count 和 depth,是即時算的,有需要這方面的功能的話,可能不適合使用。
Ransack 搜索
和 awesome_nested_set 差不多,這邊就不重複了。
效能
接著來測試一下效能,這邊我建立了一個測試專案,跑出來的結果大概是這樣 (下面整理過排版):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| --------------------------------- benchmark:create ancestry 0.114 (± 0.0%) i/s (8.79 s/i) - 1.000 in 8.786560s awesome_nested_set 0.015 (± 0.0%) i/s (65.36 s/i) - 1.000 in 65.363601s closure_tree 0.051 (± 0.0%) i/s (19.59 s/i) - 1.000 in 19.585828s --------------------------------- benchmark:update ancestry 0.046 (± 0.0%) i/s (21.83 s/i) - 1.000 in 21.831982s awesome_nested_set 1.203 (± 0.0%) i/s (831.21 ms/i) - 6.000 in 5.062957s closure_tree 0.016 (± 0.0%) i/s (60.76 s/i) - 1.000 in 60.756249s --------------------------------- benchmark:children_of ancestry 0.522 (± 0.0%) i/s (1.92 s/i) - 3.000 in 5.757987s ancestry (faster) 4.291 (± 0.0%) i/s (233.02 ms/i) - 22.000 in 5.199553s awesome_nested_set 9.551 (±10.5%) i/s (104.70 ms/i) - 48.000 in 5.047880s closure_tree 10.207 (± 9.8%) i/s (97.97 ms/i) - 51.000 in 5.046494s --------------------------------- benchmark:children ancestry 10.863 (± 9.2%) i/s (92.06 ms/i) - 53.000 in 5.008058s awesome_nested_set 7.913 (±12.6%) i/s (126.37 ms/i) - 39.000 in 5.035606s closure_tree 9.631 (±10.4%) i/s (103.83 ms/i) - 48.000 in 5.032850s --------------------------------- benchmark:descendants ancestry 6.721 (±14.9%) i/s (148.79 ms/i) - 33.000 in 5.057038s awesome_nested_set 5.415 (± 0.0%) i/s (184.69 ms/i) - 28.000 in 5.176816s closure_tree 1.003 (± 0.0%) i/s (997.09 ms/i) - 5.000 in 5.013623s --------------------------------- benchmark:siblings ancestry 12.053 (± 8.3%) i/s (82.97 ms/i) - 61.000 in 5.083198s awesome_nested_set 7.683 (± 0.0%) i/s (130.16 ms/i) - 39.000 in 5.093565s closure_tree 9.630 (±10.4%) i/s (103.84 ms/i) - 48.000 in 5.006542s --------------------------------- benchmark:ancestors ancestry 6.866 (± 0.0%) i/s (145.64 ms/i) - 35.000 in 5.107056s awesome_nested_set 5.957 (± 0.0%) i/s (167.88 ms/i) - 30.000 in 5.043287s closure_tree 0.895 (± 0.0%) i/s (1.12 s/i) - 5.000 in 5.593123s ---------------------------------
|
ancestry 大部分情況雖然效能較好,但是最常用的 children_of (依照 id 取得子節點) 的功能卻最慢。官方預設的實作方式慢了 awesome_nested_set 20 倍左右。這是因為當執行:
他實作方式是執行
會執行兩次 query。ancestry 提供類似的 class methods 都是這樣。我另外實作了一次 query 的寫法:
1 2 3
| def self.children_of_by_id(id) where(ancestry: Node.where(id: id).select(sanitize_sql_array([ "CONCAT(ancestry, ?)", "#{id}/" ]))) end
|
效能提升 10 倍左右,但還是比較慢。
總結
做成表格進行比較
| 項目 | ancestry | awesome_nested_set | closure_tree |
|---|
| 功能完整 | O | O | △ |
| 使用方便 | O | O | △ |
| 效能 | O | O | △ |
| Ransack 支援 | O | O | O |
綜合比較 ancestry 和 awesome_nested_set 較爲優秀,ancestry 在儲存資料使用的空間較小 (但移動節點時速度較慢,要把子節點全部撈出來修改。),awesome_nested_set 平均查詢效能較好。但我在測試的過程 ancestry 有遇到 bug 造成資料錯誤的情況。發生的情況是將 model record 載入到記憶體中,然後進行了多次的移動節點到不同位置。另外 awesome_nested_set 在 ransack 可以直接用 parent_id 查詢,也比較直接。所以我覺得 awesome_nested_set 可能會是比較好的選擇。